In the last post , we created a RESTful API application for simple CRUD functionalities. In this post, we will enrich it:
在上一篇文章中,我们为简单的CRUD功能创建了RESTful API应用程序。 在这篇文章中,我们将丰富它:
- Adding MongoDB support via Mongoose module 通过Mongoose模块添加MongoDB支持
- Changing dummy data storage to use MongoDB server更改虚拟数据存储以使用MongoDB服务器
- Cleaning up the testing codes清理测试代码
Let’s go.
我们走吧。
添加MongooseModule (Adding MongooseModule)
MongoDB is a leading document-based NoSQL database. Nestjs has official support for MongoDB via its Mongoosejs integration.
MongoDB是领先的基于文档的NoSQL数据库。 Nestjs通过其Mongoosejs集成对MongoDB具有官方支持。
Firstly, install the following dependencies.
首先,安装以下依赖项。
npm install --save @nestjs/mongoose mongoose
npm install --save-dev @types/mongoose
Declare a MongooseModule
in the top-level AppModule
to activate Mongoose support.
在顶级AppModule
声明一个MongooseModule
以激活Mongoose支持。
//... other imports
import { MongooseModule } from '@nestjs/mongoose';@Module({
imports: [
//...other modules imports
// add MongooseModule
MongooseModule.forRoot('mongodb://localhost/blog')
],
//... providers, controllers
})
export class AppModule {}
Mongoose requires a Schema definition to describe every document in MongoDB. Nestjs provides a simple to combine the schema definition and document POJO in the same form.
Mongoose需要一个Schema定义来描述MongoDB中的每个文档。 Nestjs提供了一种简单的方式,可以将模式定义和文档POJO以相同的形式结合在一起。
Rename our former post.interface.ts to post.model.ts, the .model suffix means it is a Mongoose managed Model
.
将我们以前的post.interface.ts重命名为post.model.ts ,后缀.model表示它是Mongoose管理的Model
。
import { SchemaFactory, Schema, Prop } from '@nestjs/mongoose';
import { Document } from 'mongoose';@Schema()
export class Post extends Document {
@Prop({ required: true })
title: string; @Prop({ required: true })
content: string; @Prop()
createdAt?: Date; @Prop()
updatedAt?: Date;
}
export const PostSchema = SchemaFactory.createForClass(Post);
The annotations @Schema
, @Prop
defines the schema definitions on the document class instead of a mongoose Schema function.
注释@Schema
, @Prop
定义了文档类上的模式定义,而不是猫鼬的Schema函数。
Open PostModule
, declare a posts
collection for storing Post
document via importing a MongooseModule.forFeature
.
打开PostModule
,通过导入MongooseModule.forFeature
声明一个posts
集合以存储Post
文档。
import { PostSchema } from './post.model';
//other imports@Module({
imports: [MongooseModule.forFeature([{ name: 'posts', schema: PostSchema }])],
// providers, controllers
})
export class PostModule {}
重构PostService (Refactoring PostService)
When a Mongoose schema (eg. PostSchame
) is mapped to a document collection(eg. posts
), a Mongoose Model
is ready for the operations of this collections in MongoDB.
当将Mongoose模式(例如PostSchame
)映射到文档集合(例如posts
)时,Mongoose Model
已准备就绪,可以在MongoDB中对该集合进行操作。
Open post.service.ts file.
打开post.service.ts文件。
Change the content to the following:
将内容更改为以下内容:
@Injectable()
export class PostService {
constructor(@InjectModel('posts') private postModel: Model<Post>) {} findAll(keyword?: string, skip = 0, limit = 10): Observable<Post[]> {
if (keyword) {
return from(
this.postModel
.find({ title: { $regex: '.*' + keyword + '.*' } })
.skip(skip)
.limit(limit)
.exec(),
);
} else {
return from(
this.postModel
.find({})
.skip(skip)
.limit(limit)
.exec(),
);
}
} findById(id: string): Observable<Post> {
return from(this.postModel.findOne({ _id: id }).exec());
} save(data: CreatePostDto): Observable<Post> {
const createPost = this.postModel.create({ ...data });
return from(createPost);
} update(id: string, data: UpdatePostDto): Observable<Post> {
return from(this.postModel.findOneAndUpdate({ _id: id }, data).exec());
} deleteById(id: string): Observable<Post> {
return from(this.postModel.findOneAndDelete({ _id: id }).exec());
} deleteAll(): Observable<any> {
return from(this.postModel.deleteMany({}).exec());
}
}
In the constructor of PostService
class, use a @InjectMock('posts')
to bind the posts
collection to a parameterized Model handler.
在PostService
类的构造函数中,使用@InjectMock('posts')
将posts
集合绑定到参数化的Model处理程序。
The usage of all mongoose related functions can be found in the official Mongoose docs.
所有与猫鼬相关的功能的用法都可以在官方的Mongoose文档中找到。
As you see, we also add two classes CreatePostDto
and UpdatePostDto
instead of the original Post
for the case of creating and updating posts.
如您所见,对于创建和更新帖子,我们还添加了两个类CreatePostDto
和UpdatePostDto
而不是原始Post
。
Following the principle separation of concerns, CreatePostDto
and UpdatePostDto
are only used for transform data from client, add readonly
modifier to keep the data immutable in the transforming progress.
按照关注点的原则分离, CreatePostDto
和UpdatePostDto
仅用于从客户端转换数据,添加readonly
修饰符以使数据在转换过程中保持不变。
// create-post.dto.ts
export class CreatePostDto {
readonly title: string;
readonly content: string;
}
// update-post.dto.ts
export class UpdatePostDto {
readonly title: string;
readonly content: string;
}
清理PostController (Cleaning up PostController)
Clean the post.controller.ts
correspondingly.
相应地清洁post.controller.ts
。
@Controller('posts')
export class PostController {
constructor(private postService: PostService) {} @Get('')
getAllPosts(
@Query('q') keyword?: string,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip?: number,
): Observable<BlogPost[]> {
return this.postService.findAll(keyword, skip, limit);
} @Get(':id')
getPostById(@Param('id') id: string): Observable<BlogPost> {
return this.postService.findById(id);
} @Post('')
createPost(@Body() post: CreatePostDto): Observable<BlogPost> {
return this.postService.save(post);
} @Put(':id')
updatePost(
@Param('id') id: string,
@Body() post: UpdatePostDto,
): Observable<BlogPost> {
return this.postService.update(id, post);
} @Delete(':id')
deletePostById(@Param('id') id: string): Observable<BlogPost> {
return this.postService.deleteById(id);
}
}
Unluckily, Mongoose APIs has no built-in Rxjs’s
Observable
support, if you are stick on Rxjs, you have to usefrom
to wrap an existingPromise
to Rxjs'sObservable
. Check this topic on stackoverflow to know more details about the difference bwteen Promise and Observable.不幸的是,Mongoose API没有内置的Rxjs的
Observable
支持,如果您坚持使用Rxjs,则必须使用from
将现有的Promise
包装到Rxjs的Observable
。 在stackoverflow上查看此主题,以了解有关Promise和Observable之间的区别的更多详细信息。
运行应用程序 (Run the application)
To run the application, a running MongoDB server is required. You can download a copy from MongoDB, and follow the installation guide to install it into your system.
要运行该应用程序,需要一个正在运行的MongoDB服务器。 您可以从MongoDB下载副本,然后按照安装指南将其安装到系统中。
Simply, prepare a docker-compose.yaml to run the dependent servers in the development stage.
简单地,准备一个docker-compose.yaml以在开发阶段运行从属服务器。
version: '3.8' # specify docker-compose version# Define the services/containers to be run
services:
mongodb:
image: mongo
volumes:
- mongodata:/data/db
ports:
- "27017:27017"
networks:
- backend
volumes:
mongodata: networks:
backend:
Run the following command to start up a mongo service in a Docker container.
运行以下命令以在Docker容器中启动mongo服务。
docker-compose up
Execute the following command in the project root folder to start up the application.
在项目根文件夹中执行以下命令以启动应用程序。
npm run start
Now open your terminal and use curl
to test the endpoints, and make sure it works as expected.
现在打开终端并使用curl
测试端点,并确保其按预期工作。
>curl http://localhost:3000/posts/
[]
There is no sample data in the MongoDB. Utilizing with the lifecycle events, it is easy to add some sample data for the test purpose.
MongoDB中没有样本数据。 利用生命周期事件,可以轻松添加一些示例数据以进行测试。
@Injectable()
export class DataInitializerService implements OnModuleInit, OnModuleDestroy {
private data: CreatePostDto[] = [
{
title: 'Generate a NestJS project',
content: 'content',
},
{
title: 'Create CRUD RESTful APIs',
content: 'content',
},
{
title: 'Connect to MongoDB',
content: 'content',
},
]; constructor(private postService: PostService) {}
onModuleInit(): void {
this.data.forEach(d => {
this.postService.save(d).subscribe(saved => console.log(saved));
});
}
onModuleDestroy(): void {
console.log('module is be destroying...');
this.postService
.deleteAll()
.subscribe(del => console.log(`deleted ${del} records.`));
}}
Register it in PostModule
.
在PostModule
注册它。
//other imports
import { DataInitializerService } from './data-initializer.service';@Module({
//imports, controllers...
providers: [//other services...
DataInitializerService],
})
export class PostModule {}
Run the application again. Now you will see some data when hinting http://localhost:3000/posts/.
再次运行该应用程序。 现在,当提示http:// localhost:3000 / posts /时,您将看到一些数据。
>curl http://localhost:3000/posts/
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0}]>curl http://localhost:3000/posts/5ee49c3115a4e75254bb732e
{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0}>curl http://localhost:3000/posts/ -d "{\"title\":\"new post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X POST
{"_id":"5ee49ca915a4e75254bb7331","title":"new post","content":"content of my new post","__v":0}>curl http://localhost:3000/posts/5ee49ca915a4e75254bb7331 -d "{\"title\":\"my updated post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X PUT
{"_id":"5ee49ca915a4e75254bb7331","title":"new post","content":"content of my new post","__v":0}>curl http://localhost:3000/posts
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0},{"_id":"5ee49ca915a4e75254bb7331","title":"my updated post","content":"content of my new post","__v":0}]>curl http://localhost:3000/posts/5ee49ca915a4e75254bb7331 -X DELETE
{"_id":"5ee49ca915a4e75254bb7331","title":"my updated post","content":"content of my new post","__v":0}>curl http://localhost:3000/posts
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0}]
清洁测试代码 (Clean the testing codes)
When switching to real data storage instead of the dummy array, we face the first issue is how to treat with the Mongo database dependency in our post.service.spec.ts.
当切换到实际数据存储而不是虚拟数组时,我们面临的第一个问题是如何在post.service.spec.ts中处理Mongo数据库依赖项。
Jest provides comprehensive mocking features to isolate the dependencies in test cases. Let’s have a look at the whole content of the refactored post.service.spec.ts file.
Jest提供了全面的模拟功能,以隔离测试用例中的依赖性。 让我们看一下重构的post.service.spec.ts文件的全部内容。
describe('PostService', () => {
let service: PostService;
let model: Model<Post>; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PostService,
{
provide: getModelToken('posts'),
useValue: {
new: jest.fn(),
constructor: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
create: jest.fn(),
remove: jest.fn(),
exec: jest.fn(),
findOneAndUpdate: jest.fn(),
findOneAndDelete: jest.fn(),
},
},
],
}).compile(); service = module.get<PostService>(PostService);
model = module.get<Model<Post>>(getModelToken('posts'));
}); it('should be defined', () => {
expect(service).toBeDefined();
}); it('getAllPosts should return 3 posts', async () => {
const posts = [
{
_id: '5ee49c3115a4e75254bb732e',
title: 'Generate a NestJS project',
content: 'content',
},
{
_id: '5ee49c3115a4e75254bb732f',
title: 'Create CRUD RESTful APIs',
content: 'content',
},
{
_id: '5ee49c3115a4e75254bb7330',
title: 'Connect to MongoDB',
content: 'content',
},
];
jest.spyOn(model, 'find').mockReturnValue({
skip: jest.fn().mockReturnValue({
limit: jest.fn().mockReturnValue({
exec: jest.fn().mockResolvedValueOnce(posts) as any,
}),
}),
} as any); const data = await service.findAll().toPromise();
expect(data.length).toBe(3);
}); it('getPostById with existing id should return 1 post', done => {
const found = {
_id: '5ee49c3115a4e75254bb732e',
title: 'Generate a NestJS project',
content: 'content',
}; jest.spyOn(model, 'findOne').mockReturnValue({
exec: jest.fn().mockResolvedValueOnce(found) as any,
} as any); service.findById('1').subscribe({
next: data => {
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
expect(data.title).toEqual('Generate a NestJS project');
},
error: error => console.log(error),
complete: done(),
});
}); it('should save post', async () => {
const toCreated = {
title: 'test title',
content: 'test content',
}; const toReturned = {
_id: '5ee49c3115a4e75254bb732e',
...toCreated,
}; jest.spyOn(model, 'create').mockResolvedValue(toReturned as Post); const data = await service.save(toCreated).toPromise();
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
expect(model.create).toBeCalledWith(toCreated);
expect(model.create).toBeCalledTimes(1);
}); it('should update post', done => {
const toUpdated = {
_id: '5ee49c3115a4e75254bb732e',
title: 'test title',
content: 'test content',
}; jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
exec: jest.fn().mockResolvedValueOnce(toUpdated) as any,
} as any); service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
next: data => {
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
},
error: error => console.log(error),
complete: done(),
});
}); it('should delete post', done => {
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
exec: jest.fn().mockResolvedValueOnce({
deletedCount: 1,
}),
} as any); service.deleteById('anystring').subscribe({
next: data => expect(data).toBeTruthy,
error: error => console.log(error),
complete: done(),
});
});
});
In the above codes,
在以上代码中,
Use a custom Provider to provide a
PostModel
dependency forPostService
, the Model is provided inuseValue
which hosted a mocked object instance for PostModel at runtime.使用自定义提供程序为
PostService
提供PostModel
依赖关系,在useValue
中提供模型,该模型在运行时托管PostModel
的模拟对象实例。In every test case, use
jest.spyOn
to assume some mocked behaviors of PostModel happened before the service is executed.在每个测试案例中,请使用
jest.spyOn
来假设PostModel的某些jest.spyOn
行为在执行服务之前发生。You can use the
toBeCalledWith
like assertions on mocked object or spied object.您可以在
toBeCalledWith
对象或间谍对象上使用类似toBeCalledWith
断言。
For me, most of time working as a Java/Spring developers, constructing such a simple Jest based test is not easy, jmcdo29/testing-nestjs is very helpful for me to jump into jest testing work.
对我来说,大多数时候以Java / Spring开发人员的身份工作,构造这样一个简单的基于Jest的测试并不容易, jmcdo29 / testing-nestjs对我进行jest测试工作非常有帮助。
The jest mock is every different from Mockito in Java. Luckily there is a ts-mockito which port Mockito to the Typescript world, check this link for more details .
笑话模拟与Java中的Mockito完全不同。 幸运的是,有一个ts-mockito可以将Mockito移植到Typescript世界,请查看此链接以获取更多详细信息。
OK, let’s move to post.controller.spec.ts.
好的,让我们转到post.controller.spec.ts 。
Similarly, PostController
depends on PostService
. To test the functionalities of PostController
, we should mock it.
同样, PostController
依赖于PostService
。 为了测试PostController
的功能,我们应该对其进行模拟。
Like the method we used in post.service.spec.ts
, we can mock it in a Provider
.
就像在post.service.spec.ts
使用的方法一样,我们可以在Provider
对其进行模拟。
describe('Post Controller(useValue jest mocking)', () => {
let controller: PostController;
let postService: PostService; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PostService,
useValue: {
findAll: jest
.fn()
.mockImplementation(
(_keyword?: string, _skip?: number, _limit?: number) =>
of<any[]>([
{
_id: 'testid',
title: 'test title',
content: 'test content',
},
]),
),
},
},
],
controllers: [PostController],
}).compile(); controller = module.get<PostController>(PostController);
postService = module.get<PostService>(PostService);
}); it('should get all posts(useValue: jest mocking)', async () => {
const result = await controller.getAllPosts('test', 10, 0).toPromise();
expect(result[0]._id).toEqual('testid');
expect(postService.findAll).toBeCalled();
expect(postService.findAll).lastCalledWith('test', 0, 10);
});
});
Instead of the jest mocking, you can use a dummy implementation directly in the Provider
.
您可以直接在Provider
使用虚拟实现,而不是开玩笑。
describe('Post Controller(useValue fake object)', () => {
let controller: PostController; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PostService,
useValue: {
findAll: (_keyword?: string, _skip?: number, _limit?: number) =>
of<any[]>([
{ _id: 'testid', title: 'test title', content: 'test content' },
]),
},
},
],
controllers: [PostController],
}).compile(); controller = module.get<PostController>(PostController);
}); it('should get all posts(useValue: fake object)', async () => {
const result = await controller.getAllPosts().toPromise();
expect(result[0]._id).toEqual('testid');
});
});
Or use fake class to replace the real PostService
in the tests.
或使用假类替换测试中的实际PostService
。
class PostServiceFake {
private posts = [
{
_id: '5ee49c3115a4e75254bb732e',
title: 'Generate a NestJS project',
content: 'content',
},
{
_id: '5ee49c3115a4e75254bb732f',
title: 'Create CRUD RESTful APIs',
content: 'content',
},
{
_id: '5ee49c3115a4e75254bb7330',
title: 'Connect to MongoDB',
content: 'content',
},
]; findAll() {
return of(this.posts);
} findById(id: string) {
const { title, content } = this.posts[0];
return of({ _id: id, title, content });
} save(data: CreatePostDto) {
return of({ _id: this.posts[0]._id, ...data });
} update(id: string, data: UpdatePostDto) {
return of({ _id: id, ...data });
} deleteById(id: string) {
return of({ ...this.posts[0], _id: id });
}
}describe('Post Controller', () => {
let controller: PostController; beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PostService,
useClass: PostServiceFake,
},
],
controllers: [PostController],
}).compile(); controller = module.get<PostController>(PostController);
}); it('should be defined', () => {
expect(controller).toBeDefined();
}); it('GET on /posts should return all posts', async () => {
const posts = await controller.getAllPosts().toPromise();
expect(posts.length).toBe(3);
}); it('GET on /posts/1 should return one post ', done => {
controller.getPostById('1').subscribe(data => {
expect(data._id).toEqual('1');
done();
});
}); it('POST on /posts should return all posts', async () => {
const post: CreatePostDto = {
title: 'test title',
content: 'test content',
};
const saved = await controller.createPost(post).toPromise();
expect(saved.title).toEqual('test title');
}); it('PUT on /posts/1 should return all posts', done => {
const post: UpdatePostDto = {
title: 'test title',
content: 'test content',
};
controller.updatePost('1', post).subscribe(data => {
expect(data.title).toEqual('test title');
expect(data.content).toEqual('test content');
done();
});
}); it('DELETE on /posts/1 should return true', done => {
controller.deletePostById('1').subscribe(data => {
expect(data).toBeTruthy();
done();
});
});
});
The above codes are more close the ones in the first article, it is simple and easy to understand.
上面的代码与第一篇文章中的代码更加接近,它简单易懂。
To ensure the fake PostService has the exact method signature of the real PostService, it is better to use an interface to define the methods if you prefer this apporach.
为确保伪造的PostService具有真实PostService的准确方法签名,如果您喜欢此方法,最好使用接口来定义方法。
I have mentioned ts-mockito, for me it is easier to boost up a Mockito like test.
我已经提到了ts-mockito ,对我来说,增强像Mockito这样的测试更容易。
npm install --save-dev ts-mockito
A simple mockito based test looks like this.
一个简单的基于模仿的测试如下所示。
// import facilites from ts-mockito
import { mock, verify, instance, anyString, anyNumber, when } from 'ts-mockito';describe('Post Controller(using ts-mockito)', () => {
let controller: PostController;
const mockedPostService: PostService = mock(PostService); beforeEach(async () => {
controller = new PostController(instance(mockedPostService));
}); it('should get all posts(ts-mockito)', async () => {
when(
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
).thenReturn(
of([
{ _id: 'testid', title: 'test title', content: 'content' },
]) as Observable<Post[]>,
);
const result = await controller.getAllPosts('', 10, 0).toPromise();
expect(result.length).toEqual(1);
expect(result[0].title).toBe('test title');
verify(
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
).once();
});
});
Now run the tests again. All tests should pass.
现在再次运行测试。 所有测试均应通过。
> npm run test... PASS src/app.controller.spec.ts
PASS src/post/post.service.spec.ts (10.307 s)
PASS src/post/post.controller.spec.ts (10.471 s)Test Suites: 3 passed, 3 total
Tests: 17 passed, 17 total
Snapshots: 0 total
Time: 11.481 s, estimated 12 s
Ran all test suites.
In this post, we connected to the real MongoDB instead of the dummy data storage, correspond to the changes , we have refactored all tests, and discuss some approaches to isolate the dependencies in tests. But we have not test all functionalities in a real integrated environment, Nestjs provides e2e testing skeleton, we will discuss it in a future post.
在本文中,我们连接到实际的MongoDB而不是虚拟数据存储,对应于更改,我们重构了所有测试,并讨论了隔离测试中依赖项的一些方法。 但是我们尚未在真正的集成环境中测试所有功能,Nestjs提供了e2e测试框架,我们将在以后的文章中进行讨论。
Grab the source codes from my github, switch to branch feat/model.
从我的github获取源代码,切换到branch feat / model 。
翻译自: https://medium.com/@hantsy/connecting-to-mongodb-c8166e0a6cb7