我可以 "include" 或在 Jest 测试中执行 TypeScript 文件吗?
Can I "include" or exec a TypeScript file in a Jest test?
我有一个 TypeScript 项目,运行使用 Express 作为后端 RESTful API。它的设计非常重对象,因此可以实例化大量 classes 并在 运行 时间和测试服务 classes 时相互注入。
我们对所有服务 classes 进行了一套很好的测试。但是,我们有一个 index.ts
将所有这些结合在一起,并且目前可以逃避测试自动化。我正在考虑多种方法来对此进行测试,以便端点和轻量级控制器免受回归影响。 (而不是列出我所有的想法可能会导致一个过于宽泛的问题,我现在将专注于一个具体的想法)。
让我展示一个我的前端控制器的例子(src/index.ts
):
/* Lots of imports here */
const app = express();
app.use(express.json());
app.use(cors());
app.options('*', cors());
/* Lots of settings from env vars here */
// Build some modules
const verificationsTableName = 'SV-Verifications';
const verificationsIndexName = 'SV-VerificationsByUserId';
const getVerificationService = new GetVerification(
docClient,
verificationsTableName,
verificationsIndexName,
timer,
EXPIRY_LENGTH,
);
const writeVerifiedStatusService = new WriteVerifiedStatus(
docClient,
verificationsTableName,
timer,
getVerificationService,
);
/* Some code omitted for brevity */
// Create some routes
GetVerificationController.createRoutes(getVerificationService, app);
FinishVerificationController.createRoutes(finishVerificationService, app);
addPostStartVerification(startVerification, app);
IsVerifiedController.createValidationRoutes(di2.createOverallFeatureFlagService(), getVerificationService, app);
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${PORT}`);
});
你明白了 - classes 是使用依赖注入组装的,我们从环境变量中获取一些配置,然后启动 HTTP 侦听器。需要注意的主要一点是此文件不包含或导出任何 classes 或函数。
我想在 Jest 测试套件中 运行 这个文件,像这样:
describe('Test endpoint wiring', () => {
beforeEach(() => {
// Set up lots of env vars
// How to run `src/index.ts` here?
});
afterEach(() => {
// Tear down the server here
});
test('First endpoint test', () => {
// Run a test against an endpoint
});
});
我想知道我是否可以在这里做一些 await exec('node command')
?我希望它在后台 运行 以便在服务器启动后测试 运行 。理想情况下,这将构成 Jest 中异步线程的一部分,但如果这不可能,那么直接生成进程可能就可以了。
如果有一种可靠的方法可以在每次测试结束时杀死它,那就太好了(我想保持 PID 并发送停止信号是可以的)。
修改 index.ts
并非不可能(实际上我想将所有这些 DI 构造塞入 class,以便可以使用简单的方法替换部分以用于测试目的遗产)。但我想先探讨一下这个不改变的选项。
如果您想保持测试套件完全独立并且仅使用 fetch
测试 HTTP 端点,就像用户一样,您可以使用 concurrently 来实现这一点。
concurrently -s first --kill-others \"yarn run serve\" \"yarn run tests:integration\"
将此命令添加到我的 package.json
时,我可以配置 serve
部分来设置快速服务器,并配置 tests:integration
脚本来实际测试端点。 -s first
使 concurrently
的返回状态与第一个退出的进程相同,并且 --kill-others
在其中一个进程完成后终止所有进程。
看到我把这些称为 集成 测试了吗?在我看来,这类测试更容易出错,它们将解决方案的这一部分作为一个整体进行测试,因此它们在 test pyramid 中处于中间水平。希望你有更多的单元测试,专注于一个一个地测试特定的 classes/files/things。
如果事情很难测试,这通常意味着您需要重构以改进封装。您需要能够在测试套件中多次创建、执行、检查和拆卸事物 运行。这意味着在所需文件的根级别执行的代码根本不可测试。
这里没有任何魔法可以使这项工作*,您只需要稍微重构一下您的代码。如果你做对了,它也会让你更容易推断出应用程序在生产环境中是如何启动的。
假设您做了更像的事情:
// Maybe move these to lib/constants.ts or something and import them instead.
const verificationsTableName = 'SV-Verifications';
const verificationsIndexName = 'SV-VerificationsByUserId';
export function boot(): Express {
const app = createExpressApp()
setupServices(app)
startApp(app)
return app
}
export function createExpressApp() {
const app = express();
// setup express app here
return app
}
export function setupServices(app: Express) {
setupGetVerificationService(app)
setupFinishVerificationService(app)
// call function that setup other services here.
}
export function setupGetVerificationService(app: Express) {
const getVerificationService = new GetVerification(/* ... */)
GetVerificationController.createRoutes(getVerificationService, app);
}
export function setupFinishVerificationService(app: Express) {
const writeVerifiedStatusService = new WriteVerifiedStatus(/* ... */)
FinishVerificationController.createRoutes(finishVerificationService, app)
}
export function startApp(app: Express) {
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${PORT}`);
});
}
export function stopApp(app: Express) {
app.close();
}
现在,在您的 index.ts
中启动您的应用程序,您可以简单地:
import { boot } from './initialize-app.ts'
boot()
现在您可以根据需要测试应用程序设置的每个步骤:
describe('Test endpoint wiring', () => {
let app: Express
beforeEach(() => {
// Set up lots of env vars
app = createExpressApp()
setupServices(app)
startApp(app)
});
afterEach(() => {
// Tear down the server here
stopApp(app)
});
test('First endpoint test', () => {
// Run a test against an endpoint
});
});
有了这种结构,您现在甚至可以只创建一个服务子集来单独测试每个服务,这也可能有助于发现服务在不应该相互依赖的地方出现的问题。作为奖励,您的测试时间也会缩短。
* 是的,您 可以 将您的服务器作为一个单独的进程进行管理,并通过 HTTP 访问其端点,但我真的不推荐这样做。如果您将其保留在一个流程中并重构您的应用程序的创建、配置和销毁方式,您的生活将会轻松得多。它会更简洁,也更灵活。
我已经为我的问题草拟了一个基于流程的答案:
import { ChildProcess, spawn } from 'child_process';
import fetch from 'node-fetch';
describe('Test endpoint wiring', () => {
let listenerProcess: ChildProcess;
beforeEach(async () => {
// @todo Don't use absolute paths here
const runner = '/root/app/node_modules/.bin/ts-node';
const listener = '/root/app/src/index.ts';
listenerProcess = spawn(runner, [listener], {
stdio: 'ignore',
detached: true,
env: {
PORT: '9001',
NODE_PATH: '/root/app/src',
},
});
listenerProcess.unref();
console.log('PID: ', listenerProcess.pid);
// @todo Add a retry loop to this
await new Promise((resolve, reject) => {
function later(delay) {
return new Promise(function (resolve) {
setTimeout(resolve, delay);
});
}
later(4000)
.then(() => {
console.log('Trying spawned listener');
return fetch('http://localhost:9001/');
})
.then(() => {
console.log('Listener seems to be up');
resolve(null);
})
.catch((error) => {
// Ignore errors (e.g. can't connect)
console.log('Cannot connect');
});
console.log('Entered promise handler');
});
});
afterEach(() => {
console.log('Do a task kill here');
listenerProcess.kill();
});
test('First endpoint test', async () => {
// FIXME Just demo code
try {
const response = await fetch('http://localhost:9001/');
console.log('HTTP response code:', response.status);
//console.log(response.headers);
console.log(await response.text()); // Not sure why this needs another await
} catch (e) {
console.error('Error has occurred:', e);
}
});
});
你可以看到它使用全局 listenerProcess
来包含生成的 index.ts
侦听器,然后该脚本的行为被传递给 [= 的虚拟环境变量修改为测试模式13=].
我用了this answer来帮助断开进程与测试父进程的连接,这样就没有挂了。您可以看到我在 9001 上旋转了监听器(9000 用于我的开发者,通常 运行 在同一个容器中)。在实践中,我会将此端口设置为 9001 + random
,以避免任何并行测试设置冲突的侦听器。
如你所见,我也在每次测试后终止了进程。这里的想法是每个测试都有一个干净的监听器,以防测试之间的监听架构中保留任何工件。
到目前为止,我的观察是,虽然这似乎可靠地打印了 PID,但有时侦听器仍然存在问题,在四秒的启动延迟后仍未准备好。从代码中可以看出,我打算实现一些重试代码,如果我坚持这个方向,我一定会这样做。但是,Alex 的解决方案让我印象深刻,因此我现在倾向于朝这个方向发展。
我有一个 TypeScript 项目,运行使用 Express 作为后端 RESTful API。它的设计非常重对象,因此可以实例化大量 classes 并在 运行 时间和测试服务 classes 时相互注入。
我们对所有服务 classes 进行了一套很好的测试。但是,我们有一个 index.ts
将所有这些结合在一起,并且目前可以逃避测试自动化。我正在考虑多种方法来对此进行测试,以便端点和轻量级控制器免受回归影响。 (而不是列出我所有的想法可能会导致一个过于宽泛的问题,我现在将专注于一个具体的想法)。
让我展示一个我的前端控制器的例子(src/index.ts
):
/* Lots of imports here */
const app = express();
app.use(express.json());
app.use(cors());
app.options('*', cors());
/* Lots of settings from env vars here */
// Build some modules
const verificationsTableName = 'SV-Verifications';
const verificationsIndexName = 'SV-VerificationsByUserId';
const getVerificationService = new GetVerification(
docClient,
verificationsTableName,
verificationsIndexName,
timer,
EXPIRY_LENGTH,
);
const writeVerifiedStatusService = new WriteVerifiedStatus(
docClient,
verificationsTableName,
timer,
getVerificationService,
);
/* Some code omitted for brevity */
// Create some routes
GetVerificationController.createRoutes(getVerificationService, app);
FinishVerificationController.createRoutes(finishVerificationService, app);
addPostStartVerification(startVerification, app);
IsVerifiedController.createValidationRoutes(di2.createOverallFeatureFlagService(), getVerificationService, app);
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${PORT}`);
});
你明白了 - classes 是使用依赖注入组装的,我们从环境变量中获取一些配置,然后启动 HTTP 侦听器。需要注意的主要一点是此文件不包含或导出任何 classes 或函数。
我想在 Jest 测试套件中 运行 这个文件,像这样:
describe('Test endpoint wiring', () => {
beforeEach(() => {
// Set up lots of env vars
// How to run `src/index.ts` here?
});
afterEach(() => {
// Tear down the server here
});
test('First endpoint test', () => {
// Run a test against an endpoint
});
});
我想知道我是否可以在这里做一些 await exec('node command')
?我希望它在后台 运行 以便在服务器启动后测试 运行 。理想情况下,这将构成 Jest 中异步线程的一部分,但如果这不可能,那么直接生成进程可能就可以了。
如果有一种可靠的方法可以在每次测试结束时杀死它,那就太好了(我想保持 PID 并发送停止信号是可以的)。
修改 index.ts
并非不可能(实际上我想将所有这些 DI 构造塞入 class,以便可以使用简单的方法替换部分以用于测试目的遗产)。但我想先探讨一下这个不改变的选项。
如果您想保持测试套件完全独立并且仅使用 fetch
测试 HTTP 端点,就像用户一样,您可以使用 concurrently 来实现这一点。
concurrently -s first --kill-others \"yarn run serve\" \"yarn run tests:integration\"
将此命令添加到我的 package.json
时,我可以配置 serve
部分来设置快速服务器,并配置 tests:integration
脚本来实际测试端点。 -s first
使 concurrently
的返回状态与第一个退出的进程相同,并且 --kill-others
在其中一个进程完成后终止所有进程。
看到我把这些称为 集成 测试了吗?在我看来,这类测试更容易出错,它们将解决方案的这一部分作为一个整体进行测试,因此它们在 test pyramid 中处于中间水平。希望你有更多的单元测试,专注于一个一个地测试特定的 classes/files/things。
如果事情很难测试,这通常意味着您需要重构以改进封装。您需要能够在测试套件中多次创建、执行、检查和拆卸事物 运行。这意味着在所需文件的根级别执行的代码根本不可测试。
这里没有任何魔法可以使这项工作*,您只需要稍微重构一下您的代码。如果你做对了,它也会让你更容易推断出应用程序在生产环境中是如何启动的。
假设您做了更像的事情:
// Maybe move these to lib/constants.ts or something and import them instead.
const verificationsTableName = 'SV-Verifications';
const verificationsIndexName = 'SV-VerificationsByUserId';
export function boot(): Express {
const app = createExpressApp()
setupServices(app)
startApp(app)
return app
}
export function createExpressApp() {
const app = express();
// setup express app here
return app
}
export function setupServices(app: Express) {
setupGetVerificationService(app)
setupFinishVerificationService(app)
// call function that setup other services here.
}
export function setupGetVerificationService(app: Express) {
const getVerificationService = new GetVerification(/* ... */)
GetVerificationController.createRoutes(getVerificationService, app);
}
export function setupFinishVerificationService(app: Express) {
const writeVerifiedStatusService = new WriteVerifiedStatus(/* ... */)
FinishVerificationController.createRoutes(finishVerificationService, app)
}
export function startApp(app: Express) {
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${PORT}`);
});
}
export function stopApp(app: Express) {
app.close();
}
现在,在您的 index.ts
中启动您的应用程序,您可以简单地:
import { boot } from './initialize-app.ts'
boot()
现在您可以根据需要测试应用程序设置的每个步骤:
describe('Test endpoint wiring', () => {
let app: Express
beforeEach(() => {
// Set up lots of env vars
app = createExpressApp()
setupServices(app)
startApp(app)
});
afterEach(() => {
// Tear down the server here
stopApp(app)
});
test('First endpoint test', () => {
// Run a test against an endpoint
});
});
有了这种结构,您现在甚至可以只创建一个服务子集来单独测试每个服务,这也可能有助于发现服务在不应该相互依赖的地方出现的问题。作为奖励,您的测试时间也会缩短。
* 是的,您 可以 将您的服务器作为一个单独的进程进行管理,并通过 HTTP 访问其端点,但我真的不推荐这样做。如果您将其保留在一个流程中并重构您的应用程序的创建、配置和销毁方式,您的生活将会轻松得多。它会更简洁,也更灵活。
我已经为我的问题草拟了一个基于流程的答案:
import { ChildProcess, spawn } from 'child_process';
import fetch from 'node-fetch';
describe('Test endpoint wiring', () => {
let listenerProcess: ChildProcess;
beforeEach(async () => {
// @todo Don't use absolute paths here
const runner = '/root/app/node_modules/.bin/ts-node';
const listener = '/root/app/src/index.ts';
listenerProcess = spawn(runner, [listener], {
stdio: 'ignore',
detached: true,
env: {
PORT: '9001',
NODE_PATH: '/root/app/src',
},
});
listenerProcess.unref();
console.log('PID: ', listenerProcess.pid);
// @todo Add a retry loop to this
await new Promise((resolve, reject) => {
function later(delay) {
return new Promise(function (resolve) {
setTimeout(resolve, delay);
});
}
later(4000)
.then(() => {
console.log('Trying spawned listener');
return fetch('http://localhost:9001/');
})
.then(() => {
console.log('Listener seems to be up');
resolve(null);
})
.catch((error) => {
// Ignore errors (e.g. can't connect)
console.log('Cannot connect');
});
console.log('Entered promise handler');
});
});
afterEach(() => {
console.log('Do a task kill here');
listenerProcess.kill();
});
test('First endpoint test', async () => {
// FIXME Just demo code
try {
const response = await fetch('http://localhost:9001/');
console.log('HTTP response code:', response.status);
//console.log(response.headers);
console.log(await response.text()); // Not sure why this needs another await
} catch (e) {
console.error('Error has occurred:', e);
}
});
});
你可以看到它使用全局 listenerProcess
来包含生成的 index.ts
侦听器,然后该脚本的行为被传递给 [= 的虚拟环境变量修改为测试模式13=].
我用了this answer来帮助断开进程与测试父进程的连接,这样就没有挂了。您可以看到我在 9001 上旋转了监听器(9000 用于我的开发者,通常 运行 在同一个容器中)。在实践中,我会将此端口设置为 9001 + random
,以避免任何并行测试设置冲突的侦听器。
如你所见,我也在每次测试后终止了进程。这里的想法是每个测试都有一个干净的监听器,以防测试之间的监听架构中保留任何工件。
到目前为止,我的观察是,虽然这似乎可靠地打印了 PID,但有时侦听器仍然存在问题,在四秒的启动延迟后仍未准备好。从代码中可以看出,我打算实现一些重试代码,如果我坚持这个方向,我一定会这样做。但是,Alex 的解决方案让我印象深刻,因此我现在倾向于朝这个方向发展。