Skip to content

Commit 93510b7

Browse files
authored
Merge pull request #35 from Pythagora-io/jest
Exporting Pythagora tests to Jest
2 parents 7d3403b + 34b1a40 commit 93510b7

25 files changed

+881
-66
lines changed

README.md

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<p align=center>
22
<picture>
33
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/10895136/228003796-7e3319ad-f0b1-4da9-a2d0-6cf67ccc7a32.png">
4-
<img height="200px" alt="Text changing depending on mode. Light: 'So light!' Dark: 'So dark!'" src="https://user-images.githubusercontent.com/10895136/228003796-7e3319ad-f0b1-4da9-a2d0-6cf67ccc7a32.png">
4+
<img height="200px" alt="Pythagora Logo" src="https://user-images.githubusercontent.com/10895136/228003796-7e3319ad-f0b1-4da9-a2d0-6cf67ccc7a32.png">
55
</picture>
66
</p>
77
<p align=center>
88
Developers spend 20-30% of their time writing tests!
99
</p>
1010
<h3 align="center">✊ Pythagora creates automated tests for you by analysing server activity ✊</h3>
11+
<h3 align="center"><a href="#exportjest"> 🤖 Generate Jest integration tests with GPT-4 and Pythagora 🤖</a></h3>
1112
<br>
1213
<p align="center">🌟 As an open source tool, it would mean the world to us if you starred Pythagora repo 🌟<br>🙏 Thank you 🙏</p>
1314
<br>
@@ -42,7 +43,9 @@ Pythagora records all requests to endpoints of your app with the response and ev
4243
<b>NOTES: </b>
4344
- to stop the capture, you can exit the process like you usually do (Eg. `Ctrl + C`)
4445
- on Windows make sure to run all commands using `Git Bash` and not `Power Shell` or anything similiar
45-
<br>
46+
47+
<br>
48+
<br>
4649
<h1 id="executingtests">▶️ Running tests</h1>
4750
When running tests, it doesn’t matter what database is your Node.js connected to or what is the state of that database. Actually, that database is never touched or used —> instead, Pythagora creates a special, ephemeral pythagoraDb database, which it uses to restore the data before each test is executed, which was present at the time when the test was recorded. Because of this, tests can be run on any machine or environment.
4851

@@ -55,6 +58,68 @@ So, after you captured all requests you want, you just need to change the mode p
5558
```
5659

5760
<br><br>
61+
<p align=center>
62+
<picture>
63+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/Pythagora-io/pythagora/assets/10895136/41f349ec-c6fe-4357-8c92-db09b88d2b8e">
64+
<img height="100px" alt="OpenAI logo" src="https://github.com/Pythagora-io/pythagora/assets/10895136/41f349ec-c6fe-4357-8c92-db09b88d2b8e">
65+
</picture>
66+
</p>
67+
<h1 id="exportjest">🤖 ️Generate Jest tests with Pythagora and GPT-4</h1>
68+
69+
You can export any Pythagora test to Jest with GPT-4. To see how it works, you can watch [the full demo video here](https://www.youtube.com/watch?v=kHbwX4QVoGY).
70+
71+
## What are Jest integration tests made of
72+
73+
- **Database setup** (before a test is run)
74+
- during the export to Jest, Pythagora saves all database documents in the `pythagora_tests/exported_tests/data` folder as a JSON file
75+
- in the `beforeEach` function, these documents are restored into the database so that the database is in the same state as it was when the test was recorded
76+
- Pythagora has built-in functions to work with the database but in case you want to use your own and completely separate Jest tests from Pythagora, use the `global-setup.js` file in which you can set up your own ways to populate the database, get a collection and clear the database
77+
- **User authentication** (when the endpoint requires authentication)
78+
- the first time you run the export, Pythagora will create `auth.js` file
79+
- it is used inside `beforeEach` function to retrieve the authentication token so that API requests (that require authentication) can be executed
80+
- **Test**
81+
- tests check the response from the API and if the database is updated correctly
82+
83+
## How to export Pythagora tests to Jest
84+
85+
1. First, you need to tell Pythagora what is the login endpoint. You can do that by running:
86+
87+
```bash
88+
npx pythagora --export-setup
89+
```
90+
91+
2. After that, just run Pythagora capture command and log into the app so the login route gets captured.
92+
93+
```bash
94+
npx pythagora --init-command "my start command" --mode capture
95+
```
96+
97+
3. Exporting to Jest is done with GPT-4 so you either need to have OpenAI API key with GPT-4 access or a Pythagora API key which you can get [here](https://mailchi.mp/f4f4d7270a7a/api-waitlist). Once you have the API key, you're ready to export tests to Jest by running:
98+
99+
```bash
100+
npx pythagora --export --test-id <TEST_ID> --openai-api-key <YOUR_OPENAI_API_KEY>
101+
```
102+
or
103+
```bash
104+
npx pythagora --export --test-id <TEST_ID> --pythagora-api-key <YOUR_PYTHAGORA_API_KEY>
105+
```
106+
107+
4. To run the exported tests, run:
108+
109+
```bash
110+
npx pythagora --mode jest
111+
```
112+
113+
Exported tests will be available in the `pythagora_tests/exported_tests` folder.
114+
115+
NOTE: Pythagora uses GPT-4 8k model so some tests that do too many things during the processing might exceed the 8k token limit. To check which tests you can export to Jest, you can run:
116+
117+
```bash
118+
npx pythagora --tests-eligible-for-export
119+
```
120+
121+
<br>
122+
<br>
58123
<h1 id="demo">🎞 Demo</h1>
59124
60125
Here are some demo videos that can help you get started.
@@ -66,6 +131,8 @@ Here are some demo videos that can help you get started.
66131
<p align=center>
67132
<a target="_blank" href="https://youtu.be/YxzvljVyaEA">Pythagora Demo (4 min)</a>
68133
<br>
134+
<a target="_blank" href="https://www.youtube.com/watch?v=kHbwX4QVoGY">Generate Jest tests with Pythagora and GPT-4 (4 min)</a>
135+
<br>
69136
<a target="_blank" href="https://youtu.be/ferEJsqBHqw">Pythagora Tech Deep Dive (16 min)</a>
70137
<br>
71138
<a target="_blank" href="https://youtu.be/opQP8NMCiPw">Dev Workflow With Pythagora (4 min)</a>
@@ -191,10 +258,42 @@ That's it! You are ready to go and all your API requests with authentication sho
191258
192259
<br><br>
193260
<h1 id="testdata">🗺️️ Where can I see the tests?</h1>
194-
Each captured test is saved in <strong><i>pythagora_tests</i></strong> directory at the root of your repository.
261+
Each captured test is saved in <strong><i>"pythagora_tests"</i></strong> directory at the root of your repository.
262+
<br><br>
263+
<details><summary style="background-color: grey; padding: 10px; border: none; border-radius: 4px; cursor: pointer;">Click here to see "pythagora_tests" folder structure explanation:</summary>
264+
265+
<ul>
266+
<li>pythagora_tests
267+
<ul>
268+
<li>exported_tests <span style="color: green;">// folder containing all exported Jest tests</span>
269+
<ul>
270+
<li>data <span style="color: green;">// folder containing Jest test data</span>
271+
<ul>
272+
<li>JestTest1.json <span style="color: green;">// this is data that is populated in DB for JestTest1.test.js</span></li>
273+
<li>JestTest2.json <span style="color: green;">// this is data that is populated in DB for JestTest2.test.js</span></li>
274+
<li>...</li>
275+
</ul>
276+
</li>
277+
<li>auth.js <span style="color: green;">// here is authentication function that is used in all Jest tests</span></li>
278+
<li>global-setup.js<span style="color: green;"> // Jest global setup if you want to use your own functions for running Jest tests</span></li>
279+
<li>JestTest1.test.js <span style="color: green;">// this is an exported Jest test</span></li>
280+
<li>JestTest2.test.js</li>
281+
<li>...</li>
282+
</ul>
283+
</li>
284+
<li>pythagoraTest1.json <span style="color: green;">// this is a Pythagora test</span></li>
285+
<li>pythagoraTest2.json</li>
286+
<li>...</li>
287+
</ul>
288+
</li>
289+
</ul>
290+
</details>
291+
<br><br>
195292
Each JSON file in this repository represents one endpoint that was captured and each endpoint can have many captured tests.
196293
If you open these files, you will see an array in which each object represents a single test. All data that's needed to run a test
197294
is stored in this object. Here is an example of a test object.
295+
<br><br>
296+
<details><summary style="background-color: grey; padding: 10px; border: none; border-radius: 4px; cursor: pointer;">Click here to see example of one recorded Pythagora test:</summary>
198297
199298
```json
200299
{
@@ -258,11 +357,8 @@ is stored in this object. Here is an example of a test object.
258357
"createdAt": "2023-02-22T14:57:52.362Z" // date when the test was captured
259358
}
260359
```
261-
<b>NOTE:</b> If you used Pythagora version < 0.0.39 tests were stored in files with delimiter "|" and since we added Windows support that is changed to "-_-".
262-
To update all your tests to work with new version of Pythagora run:
263-
```
264-
npx pythagora --rename-tests
265-
```
360+
361+
</details>
266362
267363
<br><br>
268364
<h1 id="support">🤔️ FAQ</h1>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pythagora",
3-
"version": "0.0.55",
3+
"version": "0.0.57",
44
"author": {
55
"name": "Zvonimir Sabljic",
66
"email": "[email protected]"
@@ -27,6 +27,7 @@
2727
"@mswjs/interceptors": "^0.19.2",
2828
"axios": "^1.2.2",
2929
"body-parser": "^1.20.1",
30+
"jest": "^29.5.0",
3031
"lodash": "^4.17.21",
3132
"nyc": "^15.1.0",
3233
"tryrequire": "^3.0.0",

src/Pythagora.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ const MODES = require('./const/modes.json');
22
const RedisInterceptor = require('./helpers/redis.js');
33
const { cleanupDb } = require('./helpers/mongodb.js');
44
const { makeTestRequest } = require('./helpers/testing.js');
5-
const { getPythagoraVersion } = require("./helpers/starting.js");
5+
const { getPythagoraVersion, setUpPythagoraDirs } = require("./helpers/starting.js");
66
const { logCaptureFinished, pythagoraFinishingUp } = require('./utils/cmdPrint.js');
77
const { getCircularReplacer, getMetadata, getFreePortInRange } = require('./utils/common.js');
8-
const { PYTHAGORA_TESTS_DIR, PYTHAGORA_METADATA_DIR, METADATA_FILENAME, PYTHAGORA_DELIMITER } = require('./const/common.js');
8+
const {
9+
PYTHAGORA_TESTS_DIR,
10+
PYTHAGORA_METADATA_DIR,
11+
METADATA_FILENAME,
12+
PYTHAGORA_DELIMITER,
13+
EXPORTED_TESTS_DIR,
14+
EXPORTED_TESTS_DATA_DIR
15+
} = require('./const/common.js');
916

1017
let { BatchInterceptor } = require('@mswjs/interceptors');
1118
let nodeInterceptors = require('@mswjs/interceptors/lib/presets/node.js');
@@ -39,10 +46,11 @@ class Pythagora {
3946

4047
getPythagoraVersion(this);
4148

42-
this.setUpPythagoraDirs();
49+
setUpPythagoraDirs();
4350
// this.setUpHttpInterceptor();
4451

4552
this.cleanupDone = false;
53+
this.metadata = getMetadata();
4654

4755
process.on('SIGINT', this.exit.bind(this));
4856
process.on('exit', this.exit.bind(this));
@@ -99,9 +107,8 @@ class Pythagora {
99107

100108
getTestsToExecute() {
101109
if (this.testId) return [this.testId];
102-
let metadata = getMetadata();
103-
if (!metadata || !metadata.runs) return undefined;
104-
let runs = metadata.runs;
110+
if (!this.metadata || !this.metadata.runs) return undefined;
111+
let runs = this.metadata.runs;
105112

106113
if (this.rerunAllFailed) return runs[runs.length - 1].failed;
107114

@@ -113,25 +120,18 @@ class Pythagora {
113120
let failed = _.values(this.testingRequests).filter(t => !t.passed).map(t => t.id);
114121
if (!passed.length && !failed.length) return;
115122

116-
let metadata = getMetadata();
117-
metadata.runs = (metadata.runs || []).concat([{
123+
this.metadata.runs = (this.metadata.runs || []).concat([{
118124
date: new Date(),
119125
version: this.version,
120126
passed,
121127
failed
122128
}]);
123-
metadata.runs = metadata.runs.slice(-10);
124-
metadata.initCommand = this.initCommand;
125-
fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`, JSON.stringify(metadata, getCircularReplacer(), 2));
129+
this.metadata.runs = this.metadata.runs.slice(-10);
130+
this.metadata.initCommand = this.initCommand;
131+
fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`, JSON.stringify(this.metadata, getCircularReplacer(), 2));
126132
console.log('Test run metadata saved.');
127133
}
128134

129-
setUpPythagoraDirs() {
130-
if (!fs.existsSync(`./${PYTHAGORA_TESTS_DIR}/`)) fs.mkdirSync(`./${PYTHAGORA_TESTS_DIR}/`);
131-
if (!fs.existsSync(`./${PYTHAGORA_METADATA_DIR}/`)) fs.mkdirSync(`./${PYTHAGORA_METADATA_DIR}/`);
132-
if (!fs.existsSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`)) fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`, '{}');
133-
}
134-
135135
setMongoClient(client) {
136136
this.mongoClient = client;
137137
}

src/RunPythagora.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ try {
1717
exports: require('./patches/express.js')
1818
};
1919
} catch (e) {
20-
// console.log(`Can't patch Express at ${expressPath}`);
20+
console.log(`Can't patch Express at ${expressPath}`);
2121
}
2222
}
2323

@@ -30,7 +30,7 @@ try {
3030
exports: require('./patches/mongo-client.js')(mongoPath)
3131
};
3232
} catch (e) {
33-
// console.log(`Can't patch mongodb at ${mongoPath}`);
33+
console.log(`Can't patch mongodb at ${mongoPath}`);
3434
}
3535
}
3636

@@ -40,7 +40,7 @@ try {
4040
exports: require('./patches/jwt.js')(jwtPath)
4141
};
4242
} catch (e) {
43-
// console.log(`Can't patch JWT at ${jwtPath}`);
43+
console.log(`Can't patch JWT at ${jwtPath}`);
4444
}
4545
}
4646

src/RunPythagoraTests.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
const { logTestsFinished, logTestStarting, logTestsStarting } = require('./utils/cmdPrint.js');
22
const { makeTestRequest } = require('./helpers/testing.js');
33
const { getCircularReplacer } = require('./utils/common.js')
4-
const { PYTHAGORA_TESTS_DIR, PYTHAGORA_METADATA_DIR, REVIEW_DATA_FILENAME, PYTHAGORA_DELIMITER } = require('./const/common.js');
4+
const { PYTHAGORA_METADATA_DIR, REVIEW_DATA_FILENAME, PYTHAGORA_DELIMITER, PYTHAGORA_TESTS_DIR } = require('./const/common.js');
55

66
const fs = require('fs');
77
const { exec } = require('child_process');
8+
const {getAllGeneratedTests} = require("./utils/common");
89

910
let Pythagora = global.Pythagora;
1011

@@ -19,24 +20,19 @@ function openFullCodeCoverageReport() {
1920
}
2021
}
2122

22-
async function runTests(files, testsToExecute) {
23+
async function runTests(tests, testsToExecute) {
2324
let results = [];
2425
let reviewData = [];
2526

26-
for (let file of files) {
27-
let tests = JSON.parse(fs.readFileSync(`./${PYTHAGORA_TESTS_DIR}/${file}`));
28-
for (let test of tests) {
29-
if (!testsToExecute || testsToExecute.includes(test.id)) {
30-
let { testResult, reviewJson } = await makeTestRequest(test);
31-
if (Object.keys(reviewJson).length) {
32-
reviewJson.id = test.id;
33-
reviewJson.filename = file;
34-
}
35-
36-
if (!testResult) reviewData.push(reviewJson);
37-
results.push(testResult || false);
38-
}
27+
for (let test of tests) {
28+
let { testResult, reviewJson } = await makeTestRequest(test);
29+
if (Object.keys(reviewJson).length) {
30+
reviewJson.id = test.id;
31+
reviewJson.filename = test.endpoint.replace(/\//g, PYTHAGORA_DELIMITER) + '.json';
3932
}
33+
34+
if (!testResult) reviewData.push(reviewJson);
35+
results.push(testResult || false);
4036
}
4137

4238
return { results, reviewData };
@@ -58,14 +54,13 @@ function updateReviewFile(testsToExecute, reviewData) {
5854
let error;
5955
try {
6056
const startTime = new Date();
61-
let files = fs.readdirSync(`./${PYTHAGORA_TESTS_DIR}/`);
57+
let pythagoraTests = getAllGeneratedTests();
6258
let testsToExecute = Pythagora.getTestsToExecute();
6359
if (testsToExecute && !testsToExecute.length) throw new Error('There are no tests to execute. Check if you put arguments in Pythagora command correctly.');
6460

65-
files = files.filter(f => f[0] !== '.' && f.indexOf(PYTHAGORA_DELIMITER) === 0);
66-
Pythagora.testId ? logTestStarting(Pythagora.testId) : logTestsStarting(files);
67-
68-
let { results, reviewData } = await runTests(files, testsToExecute);
61+
Pythagora.testId ? logTestStarting(Pythagora.testId) : logTestsStarting(fs.readdirSync(`./${PYTHAGORA_TESTS_DIR}/`));
62+
pythagoraTests = pythagoraTests.filter(t => !testsToExecute || testsToExecute.includes(t.id));
63+
let { results, reviewData } = await runTests(pythagoraTests);
6964

7065
let passedCount = results.filter(r => r).length,
7166
failedCount = results.filter(r => !r).length;

src/bin/run.bash

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ do
5454
then
5555
PYTHAGORA_CONFIG="$@" node "./node_modules/${pythagora_dir}/src/scripts/review.js"
5656
exit 0
57+
elif [[ "${args[$i]}" == "--tests-eligible-for-export" ]]
58+
then
59+
echo "${yellow}${bold}Tests eligible for export:${reset}"
60+
PYTHAGORA_CONFIG="$@" node "./node_modules/${pythagora_dir}/src/scripts/testsEligibleForExport.js"
61+
exit 0
62+
elif [[ "${args[$i]}" == "--export-setup" ]]
63+
then
64+
PYTHAGORA_CONFIG="$@" node "./node_modules/${pythagora_dir}/src/scripts/enterData.js"
65+
exit 0
5766
elif [[ "${args[$i]}" =~ ^--rename[-_]tests$ ]]
5867
then
5968
node "./node_modules/${pythagora_dir}/src/scripts/renameTests.js"
@@ -66,6 +75,10 @@ do
6675
then
6776
PYTHAGORA_CONFIG="$@" node "./node_modules/${pythagora_dir}/src/scripts/deleteTest.js"
6877
exit 0
78+
elif [[ "${args[$i]}" == "--export" ]]
79+
then
80+
PYTHAGORA_CONFIG="$@" node "./node_modules/${pythagora_dir}/src/commands/export.js"
81+
exit 0
6982
elif [[ "${args[$i]}" == "--mode" ]]
7083
then
7184
mode="${args[$i+1]}"

0 commit comments

Comments
 (0)