use JUnit 5 Decorator in TypeScript
在我看来,在 TypeScript 里使用面向对象是很大概率变成最常用的方式的。目前所有的 JavaScript 测试都是面向过程的,比如 qunit、jest、mocha、ava、tape 等测试框架实现,还是围绕在面向过程阶段。我以为这不是 TypeScript 在现实中该有的样式。
我对 Java 还算熟悉,比如使用 JUnit 5 的测试代码就是采用面向对象写法的,代码如下。
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
这种写法是非常简单的,这就是 Java 面向的好处。如果换成 TypeScript,几乎可以保持写法一模一样,代码如下。
import assert from "assert";
import { Test } from "ts-junit";
export default class MyFirstJUnitJupiterTests {
calculator = new Calculator();
@Test
addition() {
assert.is(2, calculator.add(1, 1));
}
}
反观前端的测试代码基本上 2 种风格。前端的测试代码风格 1,它是最常用的测试方式,代码实例如下。
test("JSON", () => {
const input = {
foo: "hello",
bar: "world",
};
const output = JSON.stringify(input);
assert.snapshot(output, `{"foo":"hello","bar":"world"}`);
assert.equal(JSON.parse(output), input, "matches original");
});
前端的测试代码风格 2,bdd 风格,它更强调行为对测试用例的影响,代码实例如下。
describe("User", function () {
describe("#save()", function () {
it("should save without error", function (done) {
var user = new User("Luna");
user.save(function (err) {
if (err) throw err;
done();
});
});
});
});
对比一下 Java 和 JavaScript 测试多个写法之后,你会发现,面向对象在 JavaScript(TypeScript)里根本不是一等公民。于是我就萌发了一个想法,想用 TypeScript 实现一下 JUnit。
jest 支持 ts 需要引入 babelts-jest 直接支持 ts,测试语法上是 jest 语法,suite/test 或 describe/it- ts-junit 支持 2 种用法,其中 cli 方式采用增量 ts 编译,效率很高的。
- ts-junit 使用 junit 5 的装饰器进行封装,成熟,使用于熟悉 OO 的开发,尤其对了解 Java 的开发者更友好。
- ts-junit 使用 uvu 作为默认策略,同时也可以实现各个常见测试框架的支持,比如 jest、mocha、ava、tape、qunit、jasmine 等(暂时未实现)。
import assert from 'assert'
import { BeforeAll, BeforeEach, Disabled, Test, AfterEach, AfterAll } from 'ts-junit'
export default class MyFirstJUnitJupiterTests {
calculator = new Calculator()
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
assert.fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assert.assumeTrue("abc".contains("Z"));
assert.fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
不依赖当前项目的 ts 环境,直接通过 cli 执行,参考源码中 tests 目录下的文件。
$ npm i --global @ts-junit/cli
$ junit tests
$ junit tests/test.ts
编写第一个测试用例
import assert from 'assert'
import { Test } from '@ts-junit/core'
export default class MyFirstJUnitJupiterTests {
calculator = new Calculator();
@Test
void addition() {
assert.is(2, calculator.add(1, 1));
}
}
$ npm i --save-dev @ts-junit/core
编写测试入口文件 ts-junit.ts,文件内指定测试文件或测试目录即可。
import * as path from "node:path";
import { run } from "@ts-junit/core";
const folder = path.resolve(process.cwd(), "./tests");
const file = path.resolve(process.cwd(), "./tests/test.ts");
run([folder, file]);
// or custom Strategy
// import SomeStrategy from "./SomeStrategy";
// run([folder, file], new SomeStrategy());
创建编译时的 tsconfig.json 文件
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"sourceMap": true,
"outDir": "./build",
"rootDir": "./src",
"typeRoots": [],
"types": [],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"exclude": ["node_modules"],
"include": ["./src/**/*.ts", "./test/**/*.ts"]
}
编辑 package.json 的启动和编译脚本
{
"scripts": {
"test": "NODE_ENV=dev ts-node --project tsconfig.json --files ts-junit.ts",
"build": "tsc"
}
}
启动服务
$ npm test
> NODE_ENV=dev ts-node --project tsconfig.json --files ts-junit.ts
[2020-9-1 19:52:12] [debug] [init] [router] get - /
- 参考 junit5 的文档 https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- 进度
7/20
Annotation | Description | isSupported |
---|---|---|
|
Denotes that a method is a test method. Unlike JUnit 4’s |
✅ |
|
Denotes that a method is a parameterized test. Such methods are inherited unless they are overridden. |
✅ |
|
Denotes that a method is a test template for a repeated test. Such methods are inherited unless they are overridden. |
❌ |
|
Denotes that a method is a test factory for dynamic tests. Such methods are inherited unless they are overridden. |
❌ |
|
Denotes that a method is a template for test cases designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. Such methods are inherited unless they are overridden. |
❌ |
|
Used to configure the test method
execution order for the annotated test class; similar to JUnit 4’s
|
❌ |
|
Used to configure the test instance lifecycle for the annotated test class. Such annotations are inherited. |
❌ |
|
Declares a custom display name for the test class or test method. Such annotations are not inherited. |
✅ |
|
Declares a custom display name generator for the test class. Such annotations are inherited. |
❌ |
|
Denotes that the annotated method should be executed before
each |
✅ |
|
Denotes that the annotated method should be executed after
each |
✅ |
|
Denotes that the annotated method should be executed before
all |
✅ |
|
Denotes that the annotated method should be executed after
all |
✅ |
|
Denotes that the annotated class is a non-static nested test class. |
❌ |
|
Used to declare tags for filtering tests, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level. |
❌ |
|
Used to disable a test class or test
method; analogous to JUnit 4’s |
✅ |
|
Used to fail a test, test factory, test template, or lifecycle method if its execution exceeds a given duration. Such annotations are inherited. |
❌ |
|
Used to register extensions declaratively. Such annotations are inherited. |
❌ |
|
Used to register extensions programmatically via fields. Such fields are inherited unless they are shadowed. |
❌ |
|
Used to supply a temporary directory via field
injection or parameter injection in a lifecycle method or test method; located in the
|
❌ |
-
结合 https://github.com/midwayjs/injection 更简单(暂未实现)
class Test { @Inject() helloTest: IHelloTest; @Inject() helloService: IHelloService; @Before() before() { mock(helloTest, "sayhello", () => { return "mocked"; }); } @Test() async test() { expect(this.helloTest.sayhello()).eq("mocked"); expect(this.helloService.sayhello("test")).eq("hello test"); } }
-
use vm2 with require from memfs