Reputation: 9565
I find Karma js tests somewhat cumbersome to set up and write and find myself often ignoring writing tests because of this, so I wanted to know if there exist better alternatives.
Since I use typescript my dream scenario would be if I could write something like this:
module adder {
export function add(a, b){
return a + b;
}
}
[Tests]
assert.equal(4, adder.add(2, 2));
My tests are inline and would be run directly in my editor when changes in current file occur. Since typescript could easily remove tests from final output I could put my tests in the same file as my code (The closer the better in my opinion). Does any testing framework support this and if not what would be needed to support this scenario.
Upvotes: 2
Views: 835
Reputation: 81
I suppose that any framework which declares tests as ordinary functions or objects can be made to support inline tests. You could declare the tests in Jasmine syntax as in blorkfish's answer alongside the code being tested. However I don't think a framework is needed.
It's worth remembering that the top level statements in a Type/JavaScript file are executed when it's imported (unless it is a type only import). So you can add test functions or objects to a global list of tests. Once all modules have been imported you can then run the tests.
For example let's say we have lib/test.ts
interface Suites {
[index: string]: Tests;
};
interface Tests {
[index: string]: () => Promise<void> | void;
};
let suites: Suites = {};
export function suite(name: string)
{
const suite: Tests = {};
suites[name] = suite;
return suite;
}
export async function run(on_pass: (desc: string) => void,
on_fail: (desc: string, error: Error) => void)
{
for (let suite in suites) {
for (let test in suites[suite]) {
const desc = `${suite}: ${test}`;
try {
// this will run the tests in serial
await suites[suite][test]();
on_pass(desc);
} catch (e) {
on_fail(desc, e as Error);
}
}
}
}
Then we can define a test like so
import { strict as assert } from 'node:assert';
import { inspect } from 'node:util';
import { suite } from 'lib/test';
// Create a test suite/group
const test = suite('lib/google');
// Not important
export type DateTime = { dateTime: string, };
export type DateOnly = { date: string, };
export function json2date(dt: DateTime | DateOnly)
{
// left as an exercise to the reader
}
// Add the test to the suite
test['json2date'] = () => {
const dt = {
dateTime: '2023-01-02T14:42:25.861Z',
};
const d = {
date: '2026-01-01',
};
assert.deepEqual(json2date(dt), new Date(dt.dateTime));
assert.deepEqual(json2date(d), new Date(2026, 0, 1));
};
test['An async test'] = async () => {
// test some async code
};
Then we need to load and execute all the tests. I do this as part of the apps health checks. That is, when a GET request is made to /sys/health
, some code like the following is run
import { ... } from 'lib/google';
import { run as run_tests } from 'lib/test';
...
app.get('/sys/health', async () => {
let passes: number = 0;
let fails: number = 0;
const on_pass = (desc: string) => {
passes++;
log.info(`Test PASS: ${desc}`);
};
const on_fail = (desc: string, err: Error) => {
fails++;
log.error(`Test FAIL: ${desc}: ${inspect(err)}`);
};
await run_tests(on_pass, on_fail);
log.info(`Test summary: ${passes} passes, ${fails} fails, ${passes + fails} in total`);
if (fails > 0)
// return 500 to cause health checks to fail
});
Similar code can be used on the client. Indeed if a user experiences a client side error you can automatically run the client side tests in the background of the error page. The tests that will get run will depend on what modules were included.
The downside to this is that the test code will get included alongside the ordinary code. This isn't an issue if you run your unit tests in production as health checks. Also most bundlers support 'defines' and dead code elimination, so you can remove them from production code (e.g. https://esbuild.github.io/api/#define).
Upvotes: 0
Reputation: 22894
Just a pedantic note - Karma is a test runner, not a test framework. However, it can use Jasmine, Mocha, QUnit or roll-your-own test framework.
You might be able to use decorator syntax to accomplish this type of behaviour. Something like:
@TestSuite
class AdderTests {
@Test
test1() {
assert.equal(4, adder.add(2,2));
}
@Test
test2() {
assert.equal(0, adder.add(2,-2));
}
}
Having said this, though, the structure of the test code is very similar to Jasmine syntax:
describe('AdderTests', () => {
it('should add 2 and 2', () => {
expect(adder.add(2,2)).toBe(4);
}
});
Your ideal, i.e:
[Test]
assert.equal( ... )
Is not really possible using TypeScript decorators, or even generics. You are trying to apply an attribute to an arbitrary line of code. This would have to be a function for a start, in order to correctly use JavaScript scoping rules:
[Test]
test1() { assert.equal( ... ) };
Upvotes: 2