diff --git a/docs/menus.ts b/docs/menus.ts index 96cbbf77a..95c8c09de 100644 --- a/docs/menus.ts +++ b/docs/menus.ts @@ -81,11 +81,11 @@ export default { children: ['/development/http-api/index', '/development/http-api/rest-api'], }, { - title: 'Testing', - 'title.zh-CN': '单元测试', + title: 'Others', + 'title.zh-CN': '其他', type: 'group', children: [ - '/development/pre-release/test', + '/development/others/testing', // '/development/pre-release/build', ], }, diff --git a/docs/zh-CN/development/pre-release/build.md b/docs/zh-CN/development/others/build.md similarity index 100% rename from docs/zh-CN/development/pre-release/build.md rename to docs/zh-CN/development/others/build.md diff --git a/docs/zh-CN/development/others/testing.md b/docs/zh-CN/development/others/testing.md new file mode 100644 index 000000000..49300ee59 --- /dev/null +++ b/docs/zh-CN/development/others/testing.md @@ -0,0 +1,163 @@ +# 单元测试 + +## 介绍 + +NocoBase 的测试基于 [Jest](https://jestjs.io/) 测试框架。同时,为了方便的编写测试,我们提供了两个工具类,在测试环境模拟正常的数据库和应用的服务端。 + +### MockDatabase + +模拟数据库类继承自 [`Database`](/api/database) 类,大部分内容没有区别,主要在构造函数默认内置了随机表前缀,在每个测试用例初始化数据库时相关数据表都通过前缀名称与其他用例进行隔离,在运行测试用例时互不影响。 + +```ts +import { MockDatabase } from '@nocobase/test'; + +describe('my suite', () => { + let db; + + beforeEach(async () => { + db = new MockDatabase(); + + db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title', + } + ] + }); + + await db.sync(); + }); + + test('my case', async () => { + const postRepository = db.getRepository('posts'); + const p1 = await postRepository.create({ + values: { + title: 'hello' + } + }); + + expect(p1.get('title')).toEqual('hello'); + }); +}); +``` + +### MockServer + +模拟服务器也继承自 [Application](/api/server/application) 类,除了内置的数据库实例是通过模拟数据库类生成的以外,还提供了比较方便的生成基于 [superagent](https://www.npmjs.com/package/superagent) 请求代理功能,针对从发送请求到获取响应的写法也集成了 `.resource('posts').create()`,比较简化。 + +```ts +import { mockServer } from '@nocobase/test'; + +describe('my suite', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = mockServer(); + agent = app.agent(); + + db.collection({ + name: 'posts', + fields: [ + { + type: 'string', + name: 'title', + } + ] + }); + + await db.sync(); + await app.load(); + }); + + test('my case', async () => { + const { body } = await agent.resource('posts').create({ + values: { + title: 'hello' + } + }); + + expect(body.data.title).toEqual('hello'); + }); +}); +``` + +## 示例 + +我们以之前在 [资源与操作](development/guide/resources-actions) 章节的功能为例,来写一个插件的测试: + +```ts +import { mockServer } from '@nocobase/test'; +import Plugin from '../../src/server'; + +describe('shop actions', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = mockServer(); + app.plugin(Plugin); + agent = app.agent(); + db = app.db; + + await app.load(); + await db.sync(); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('product order case', async () => { + const { body: product } = await agent.resource('products').create({ + values: { + title: 'iPhone 14 Pro', + price: 7999, + enabled: true, + inventory: 1 + } + }); + expect(product.data.price).toEqual(7999); + + const { body: order } = await agent.resource('orders').create({ + values: { + productId: product.data.id + } + }); + expect(order.data.totalPrice).toEqual(7999); + expect(order.data.status).toEqual(0); + + const { body: deliveredOrder } = await agent.resource('orders').deliver({ + values: { + orderId: order.data.id, + provider: 'SF', + trackingNumber: '123456789' + } + }); + expect(deliveredOrder.data.status).toBe(2); + expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789'); + }); +}); +``` + +编写完成后,在命令行中允许测试命令: + +```bash +yarn test packages/samples/shop-actions +``` + +该测试将验证: + +1. 商品可以创建成功; +2. 订单可以创建成功; +3. 订单可以发货成功; + +当然这只是个最基本的例子,从业务上来说并不完善,但作为示例已经可以说明整个测试的流程。 + +## 小结 + +本章涉及的示例代码整合在对应的包 [packages/samples/shop-actions](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-actions) 中,可以直接在本地运行,查看效果。 diff --git a/docs/zh-CN/development/pre-release/test.md b/docs/zh-CN/development/pre-release/test.md deleted file mode 100644 index 94cfd7d77..000000000 --- a/docs/zh-CN/development/pre-release/test.md +++ /dev/null @@ -1 +0,0 @@ -# Testing \ No newline at end of file diff --git a/packages/samples/shop-actions/__tests__/server/index.test.ts b/packages/samples/shop-actions/__tests__/server/index.test.ts new file mode 100644 index 000000000..12956db08 --- /dev/null +++ b/packages/samples/shop-actions/__tests__/server/index.test.ts @@ -0,0 +1,52 @@ +import { mockServer } from '@nocobase/test'; +import Plugin from '../../src/server'; + +describe('shop actions', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = mockServer(); + app.plugin(Plugin); + agent = app.agent(); + db = app.db; + + await app.load(); + await db.sync(); + }); + + afterEach(async () => { + await app.destroy(); + }); + + test('product order case', async () => { + const { body: product } = await agent.resource('products').create({ + values: { + title: 'iPhone 14 Pro', + price: 7999, + enabled: true, + inventory: 1 + } + }); + expect(product.data.price).toEqual(7999); + + const { body: order } = await agent.resource('orders').create({ + values: { + productId: product.data.id + } + }); + expect(order.data.totalPrice).toEqual(7999); + expect(order.data.status).toEqual(0); + + const { body: deliveredOrder } = await agent.resource('orders').deliver({ + values: { + orderId: order.data.id, + provider: 'SF', + trackingNumber: '123456789' + } + }); + expect(deliveredOrder.data.status).toBe(2); + expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789'); + }); +}); diff --git a/packages/samples/shop-actions/src/server/index.ts b/packages/samples/shop-actions/src/server/index.ts index 624306ed0..689563127 100644 --- a/packages/samples/shop-actions/src/server/index.ts +++ b/packages/samples/shop-actions/src/server/index.ts @@ -42,9 +42,24 @@ export class ShopPlugin extends Plugin { return ctx.throw(404); } + ctx.state.product = product; + await next(); } - ] + ], + async handler(ctx, next) { + const { product } = ctx.state; + const order = await ctx.db.getRepository('orders').create({ + values: { + ...ctx.action.params.values, + productId: product.id, + quantity: 1, + totalPrice: product.price + } + }); + + ctx.body = order; + } }, list: { filter: { @@ -70,6 +85,7 @@ export class ShopPlugin extends Plugin { } } }); + order.delivery = await order.getDelivery(); ctx.body = order;