Skip to content

Commit 615f7b4

Browse files
committed
Update unittest bundle and tab to allow multiple and nested calls to describe
1 parent b5e8328 commit 615f7b4

File tree

5 files changed

+295
-173
lines changed

5 files changed

+295
-173
lines changed

src/bundles/unittest/src/__tests__/index.ts

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,102 @@
11
import { list } from 'js-slang/dist/stdlib/list';
2-
import { beforeEach, expect, test, vi } from 'vitest';
2+
import { beforeEach, describe, expect, test, vi } from 'vitest';
33
import * as asserts from '../asserts';
44
import * as testing from '../functions';
55

6-
beforeEach(() => {
7-
testing.testContext.suiteResults = {
8-
name: '',
9-
results: [],
10-
total: 0,
11-
passed: 0,
12-
};
13-
testing.testContext.allResults.results = [];
14-
testing.testContext.runtime = 0;
15-
testing.testContext.called = false;
16-
});
6+
vi.spyOn(performance, 'now').mockReturnValue(0);
177

18-
test('context is created correctly', () => {
19-
const mockTestFn = vi.fn();
20-
testing.describe('Testing 321', () => {
21-
testing.it('Testing 123', mockTestFn);
8+
describe('Test \'it\' and \'describe\'', () => {
9+
beforeEach(() => {
10+
testing.suiteResults.splice(0);
2211
});
23-
expect(testing.testContext.suiteResults.passed).toEqual(1);
24-
expect(mockTestFn).toHaveBeenCalled();
25-
});
2612

27-
test('context fails correctly', () => {
28-
testing.describe('Testing 123', () => {
29-
testing.it('This test fails!', () => asserts.assert_equals(0, 1));
13+
test('it() throws an error when called without describe', () => {
14+
expect(() => testing.it('desc', () => {})).toThrowError('\'it\' must be called from within a test suite!');
15+
});
16+
17+
test('it() throws an error even after describe', () => {
18+
testing.describe('a test', () => {});
19+
expect(() => testing.it('desc', () => {})).toThrowError('\'it\' must be called from within a test suite!');
20+
});
21+
22+
test('it() works fine from within a describe block', () => {
23+
expect(() => {
24+
testing.describe('desc', () => {
25+
testing.it('desc', () => {});
26+
});
27+
}).not.toThrow();
28+
});
29+
30+
test('it() correctly assigns results to the correct suite', () => {
31+
testing.describe('block1', () => {
32+
testing.it('test1', () => {});
33+
});
34+
35+
testing.describe('block2', () => {
36+
testing.it('test2', () => {});
37+
});
38+
39+
expect(testing.suiteResults.length).toEqual(2);
40+
const [result1, result2] = testing.suiteResults;
41+
expect(result1).toMatchObject({
42+
name: 'block1',
43+
results: [{ name: 'test1', passed: true }],
44+
passed: true,
45+
passCount: 1,
46+
runtime: 0,
47+
});
48+
49+
expect(result2).toMatchObject({
50+
name: 'block2',
51+
results: [{ name: 'test2', passed: true }],
52+
runtime: 0,
53+
passed: true,
54+
passCount: 1
55+
});
56+
});
57+
58+
test('it() correctly assigns results to child suites', () => {
59+
testing.describe('block1', () => {
60+
testing.describe('block3', () => {
61+
testing.it('test3', () => {});
62+
});
63+
testing.it('test1', () => {});
64+
});
65+
66+
testing.describe('block2', () => {
67+
testing.it('test2', () => {});
68+
});
69+
70+
expect(testing.suiteResults.length).toEqual(2);
71+
const [result1, result2] = testing.suiteResults;
72+
// Verify Result 1 first
73+
expect(result1.results.length).toEqual(2);
74+
const [subResult1, subResult2] = result1.results;
75+
expect(subResult1).toMatchObject({
76+
name: 'block3',
77+
results: [{ name: 'test3', passed: true }],
78+
runtime: 0,
79+
passCount: 1,
80+
passed: true
81+
});
82+
83+
expect(subResult2).toEqual({
84+
name: 'test1',
85+
passed: true
86+
});
87+
88+
expect(result1.name).toEqual('block1');
89+
expect(result1.runtime).toEqual(0);
90+
91+
// Verify result2 next
92+
expect(result2).toMatchObject({
93+
name: 'block2',
94+
results: [{ name: 'test2', passed: true }],
95+
runtime: 0,
96+
passCount: 1,
97+
passed: true,
98+
});
3099
});
31-
expect(testing.testContext.suiteResults.passed).toEqual(0);
32-
expect(testing.testContext.suiteResults.total).toEqual(1);
33100
});
34101

35102
test('assert works', () => {
Lines changed: 86 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,112 @@
11
import context from 'js-slang/context';
22

3-
import type { Test, TestContext, TestSuite } from './types';
3+
import type { Suite, SuiteResult, Test, TestResult } from './types';
44

5-
const handleErr = (err: any) => {
5+
function getNewSuite(name?: string): Suite {
6+
return {
7+
name,
8+
results: [],
9+
};
10+
}
11+
12+
/**
13+
* If describe was called multiple times from the root level, we need somewhere
14+
* to collect those Suite Results since none of them will have a parent suite
15+
*/
16+
export const suiteResults: SuiteResult[] = [];
17+
let currentSuite: Suite | null = null;
18+
19+
function handleErr(err: any) {
620
if (err.error && err.error.message) {
721
return (err.error as Error).message;
822
}
923
if (err.message) {
1024
return (err as Error).message;
1125
}
1226
throw err;
13-
};
14-
15-
export const testContext: TestContext = {
16-
called: false,
17-
describe(msg: string, suite: TestSuite) {
18-
if (this.called) {
19-
throw new Error(`${describe.name} can only be called once per program!`);
20-
}
21-
22-
this.called = true;
23-
24-
const starttime = performance.now();
25-
this.suiteResults = {
26-
name: msg,
27-
results: [],
28-
total: 0,
29-
passed: 0,
30-
};
31-
32-
suite();
33-
34-
this.allResults.results.push(this.suiteResults);
35-
36-
const endtime = performance.now();
37-
this.runtime += endtime - starttime;
38-
return this.allResults;
39-
},
40-
41-
it(msg: string, test: Test) {
42-
const name = `${msg}`;
43-
let error = '';
44-
this.suiteResults.total += 1;
27+
}
4528

46-
try {
47-
test();
48-
this.suiteResults.passed += 1;
49-
} catch (err: any) {
50-
error = handleErr(err);
51-
}
29+
/**
30+
* Defines a single test.
31+
* @param name Description for this test.
32+
* @param func Function containing assertions.
33+
*/
34+
export function it(name: string, func: Test): void {
35+
if (currentSuite === null) {
36+
throw new Error(`'${it.name}' must be called from within a test suite!`);
37+
}
5238

53-
this.suiteResults.results.push({
39+
try {
40+
func();
41+
currentSuite.results.push({
5442
name,
43+
passed: true,
44+
});
45+
} catch (err) {
46+
const error = handleErr(err);
47+
currentSuite.results.push({
48+
name,
49+
passed: false,
5550
error,
5651
});
57-
},
58-
59-
suiteResults: {
60-
name: '',
61-
results: [],
62-
total: 0,
63-
passed: 0,
64-
},
65-
66-
allResults: {
67-
results: [],
68-
toReplString: () =>
69-
`${testContext.allResults.results.length} suites completed in ${testContext.runtime} ms.`,
70-
},
71-
72-
runtime: 0,
73-
};
74-
75-
context.moduleContexts.unittest.state = testContext;
52+
}
53+
}
7654

7755
/**
7856
* Defines a single test.
79-
* @param str Description for this test.
80-
* @param func Function containing tests.
57+
* @param msg Description for this test.
58+
* @param func Function containing assertions.
8159
*/
82-
export function it(msg: string, func: Test) {
83-
testContext.it(msg, func);
60+
export function test(msg: string, func: Test): void {
61+
if (currentSuite === null) {
62+
throw new Error(`${test.name} must be called from within a test suite!`);
63+
}
64+
it(msg, func);
65+
}
66+
67+
function determinePassCount(results: (TestResult | SuiteResult)[]): number {
68+
const passedItems = results.filter(each => {
69+
if ('results' in each) {
70+
const passCount = determinePassCount(each.results);
71+
each.passed = passCount === each.results.length;
72+
}
73+
74+
return each.passed;
75+
});
76+
77+
return passedItems.length;
8478
}
8579

8680
/**
8781
* Describes a test suite.
88-
* @param str Description for this test.
82+
* @param msg Description for this test suite.
8983
* @param func Function containing tests.
9084
*/
91-
export function describe(msg: string, func: TestSuite) {
92-
return testContext.describe(msg, func);
85+
export function describe(msg: string, func: Test): void {
86+
const oldSuite = currentSuite;
87+
const newSuite = getNewSuite(msg);
88+
89+
currentSuite = newSuite;
90+
newSuite.startTime = performance.now();
91+
func();
92+
currentSuite = oldSuite;
93+
94+
const passCount = determinePassCount(newSuite.results);
95+
const suiteResult: SuiteResult = {
96+
name: msg,
97+
results: newSuite.results,
98+
passCount,
99+
passed: passCount === newSuite.results.length,
100+
runtime: performance.now() - newSuite.startTime
101+
};
102+
103+
if (oldSuite !== null) {
104+
oldSuite.results.push(suiteResult);
105+
} else {
106+
suiteResults.push(suiteResult);
107+
}
93108
}
109+
110+
context.moduleContexts.unittest.state = {
111+
suiteResults
112+
};

src/bundles/unittest/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export {
1212
assert_greater_equals,
1313
assert_length,
1414
} from './asserts';
15-
export { it, describe } from './functions';
15+
export { it, test, describe } from './functions';
1616
export { mock_fn } from './mocks';
1717

1818
/**

src/bundles/unittest/src/types.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,45 @@ export type ErrorLogger = (
22
error: string[] | string,
33
isSlangError?: boolean
44
) => void;
5+
6+
/**
7+
* Represents a function that when called, should either execute successfully
8+
* or throw an assertion error
9+
*/
510
export type Test = () => void;
11+
12+
/**
13+
* Represents a function that when called, executes any number of {@link Test|tests}
14+
*/
615
export type TestSuite = () => void;
7-
export type TestContext = {
8-
called: boolean;
9-
describe: (msg: string, tests: TestSuite) => Results;
10-
it: (msg: string, test: Test) => void;
11-
// This holds the result of a single suite and is cleared on every run
12-
suiteResults: SuiteResult;
13-
// This holds the results of the entire suite
14-
allResults: Results;
16+
17+
export interface Suite {
18+
name?: string
19+
results: (TestResult | SuiteResult)[]
20+
startTime?: number,
21+
}
22+
23+
export interface SuiteResult {
24+
name: string,
25+
results: (TestResult | SuiteResult)[]
1526
runtime: number;
27+
passCount: number
28+
passed: boolean
29+
}
30+
31+
export type TestSuccess = {
32+
passed: true
33+
name: string
1634
};
17-
export type TestResult = {
18-
name: string;
19-
error: string;
20-
};
21-
export type SuiteResult = {
22-
name: string;
23-
results: TestResult[];
24-
total: number;
25-
passed: number;
26-
};
27-
export type Results = {
28-
results: SuiteResult[];
29-
toReplString: () => string;
35+
36+
export type TestFailure = {
37+
passed: false
38+
name: string
39+
error: string
3040
};
41+
42+
export type TestResult = TestSuccess | TestFailure;
43+
44+
export interface UnittestModuleState {
45+
suiteResults: SuiteResult[]
46+
}

0 commit comments

Comments
 (0)