mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
feat(mongo): connect to mongo
This commit is contained in:
parent
e37124bfd0
commit
0c985e505f
@ -5,6 +5,7 @@ A NestJS RESTful APIs sample project.
|
||||
## Docs
|
||||
|
||||
* [Building RESTful APIs with NestJS](./docs/guide.md)
|
||||
* [Connecting to MongoDB](./docs/mongo.md)
|
||||
|
||||
## Build
|
||||
|
||||
@ -44,4 +45,6 @@ $ npm run test:cov
|
||||
|
||||
## Reference
|
||||
|
||||
* [The official Nestjs documentation](https://docs.nestjs.com/first-steps)
|
||||
* [The official Nestjs documentation](https://docs.nestjs.com/first-steps)
|
||||
* [Unit testing NestJS applications with Jest](https://blog.logrocket.com/unit-testing-nestjs-applications-with-jest/)
|
||||
* [ts-mockito: Mocking library for TypeScript inspired by http://mockito.org/](https://github.com/NagRock/ts-mockito)
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
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:
|
||||
728
docs/mongo.md
Normal file
728
docs/mongo.md
Normal file
@ -0,0 +1,728 @@
|
||||
# Connecting to MongoDB
|
||||
|
||||
In the last post , we created a RESTful API application for simple CRUD functionalities. In this post, we will enrich it:
|
||||
|
||||
* Add Mongo support
|
||||
* Change dummy data storage to use MongoDB
|
||||
* Clean the testing codes
|
||||
|
||||
Let's go.
|
||||
|
||||
|
||||
|
||||
## Adding MongooseModule
|
||||
|
||||
[MongoDB](https://www.mongodb.com) is a leading document-based NoSQL database. Nestjs has official support for MongoDB via its [Mongoosejs](https://mongoosejs.com/) integration.
|
||||
|
||||
Firstly, install the following dependencies.
|
||||
|
||||
```bash
|
||||
npm install --save @nestjs/mongoose mongoose
|
||||
npm install --save-dev @types/mongoose
|
||||
```
|
||||
|
||||
Declare a `MongooseModule ` in the top-level `AppModule` to activate Mongoose support.
|
||||
|
||||
```typescript
|
||||
//... 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.
|
||||
|
||||
Rename our former *post.interface.ts* to *post.model.ts*, the *.model* suffix means it is a Mongoose managed `Model`.
|
||||
|
||||
```typescript
|
||||
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](https://mongoosejs.com/docs/guide.html#definition) function.
|
||||
|
||||
Open `PostModule`, declare a `posts` collection for storing `Post` document via importing a `MongooseModule.forFeature`.
|
||||
|
||||
```
|
||||
import { PostSchema } from './post.model';
|
||||
//other imports
|
||||
|
||||
@Module({
|
||||
imports: [MongooseModule.forFeature([{ name: 'posts', schema: PostSchema }])],
|
||||
// providers, controllers
|
||||
})
|
||||
export class PostModule {}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
Open *post.service.ts* file.
|
||||
|
||||
Change the content to the following:
|
||||
|
||||
```typescript
|
||||
@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.
|
||||
|
||||
The usage of all mongoose related functions can be found in [the official Mongoose docs](https://mongoosejs.com/docs/api/query.html).
|
||||
|
||||
As you see, we also add two classes `CreatePostDto` and `UpdatePostDto` instead of the original `Post` for the case of creating and updating posts.
|
||||
|
||||
Following the principle [separation of concerns](https://en.wikipedia.org/wiki/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.
|
||||
|
||||
```typescript
|
||||
// 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;
|
||||
}
|
||||
```
|
||||
|
||||
## Clean up PostController
|
||||
|
||||
Clean the `post.controller.ts` correspondingly.
|
||||
|
||||
```typescript
|
||||
@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 use `from` to wrap an existing `Promise` to Rxjs's `Observable`. Check [this topic on stackoverflow to know more details about the difference bwteen Promise and Observable](https://stackoverflow.com/questions/37364973/what-is-the-difference-between-promises-and-observables).
|
||||
|
||||
## Run the application
|
||||
|
||||
To run the application, a running MongoDB server is required. You can download a copy from [MongoDB](https://www.mongodb.com), and follow the [installation guide](https://docs.mongodb.com/manual/installation/) to install it into your system.
|
||||
|
||||
Simply, prepare a *docker-compose.yaml* to run the dependent servers in the development stage.
|
||||
|
||||
```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.
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Execute the following command in the project root folder to start up the application.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
Now open your terminal and use `curl` to test the endpoints, and make sure it works as expected.
|
||||
|
||||
```bash
|
||||
>curl http://localhost:3000/posts/
|
||||
[]
|
||||
```
|
||||
|
||||
There is no sample data in the MongoDB. Utilizing with [the lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events), it is easy to add some sample data for the test purpose.
|
||||
|
||||
```typescript
|
||||
@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`.
|
||||
|
||||
```typescript
|
||||
//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/*.
|
||||
|
||||
```bash
|
||||
>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*.
|
||||
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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 for `PostService`, the Model is provided in `useValue` which hosted a mocked object instance for PostModel at runtime.
|
||||
* In every test case, use `jest.spyOn` to assume some mocked behaviors of PostModel happened before the service is executed.
|
||||
* You can use the `toBeCalledWith` like assertions on mocked object or spied object.
|
||||
|
||||
> For me, most of time working as a Java/Spring developers, constructing such a simple Jest based test is not easy, [jmcdo29/testing-nestjs](https://github.com/jmcdo29/testing-nestjs) is very helpful for me to jump into jest testing work.
|
||||
>
|
||||
> 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](https://github.com/NagRock/ts-mockito) for more details .
|
||||
|
||||
OK, let's move to *post.controller.spec.ts*.
|
||||
|
||||
Similarly, `PostController` depends on `PostService`. To test the functionalities of `PostController`, we should mock it.
|
||||
|
||||
Like the method we used in `post.service.spec.ts`, we can mock it in a `Provider`.
|
||||
|
||||
```typescript
|
||||
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`.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
I have mentioned *ts-mockito*, for me it is easier to boost up a Mockito like test.
|
||||
|
||||
```bash
|
||||
npm install --save-dev ts-mockito
|
||||
```
|
||||
A simple mockito based test looks like this.
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```bash
|
||||
> 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.
|
||||
221
package-lock.json
generated
221
package-lock.json
generated
@ -1759,6 +1759,11 @@
|
||||
"uuid": "8.0.0"
|
||||
}
|
||||
},
|
||||
"@nestjs/mongoose": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/@nestjs/mongoose/download/@nestjs/mongoose-7.0.1.tgz",
|
||||
"integrity": "sha1-gy7HJiHMkQPAPcKUgYwa50mkg+M="
|
||||
},
|
||||
"@nestjs/platform-express": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npm.taobao.org/@nestjs/platform-express/download/@nestjs/platform-express-7.1.2.tgz",
|
||||
@ -1887,6 +1892,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/bson": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/bson/download/@types/bson-4.0.2.tgz",
|
||||
"integrity": "sha1-esy4WUL8ObvbdRXU3kN8BPaYEV8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/color-name/download/@types/color-name-1.1.1.tgz?cache=0&sync_timestamp=1588200011932&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fcolor-name%2Fdownload%2F%40types%2Fcolor-name-1.1.1.tgz",
|
||||
@ -2005,6 +2019,26 @@
|
||||
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mongodb": {
|
||||
"version": "3.5.20",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/mongodb/download/@types/mongodb-3.5.20.tgz",
|
||||
"integrity": "sha1-QGWvxVtiddDdjoMfpCEBhHXVTVY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/bson": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/mongoose": {
|
||||
"version": "5.7.24",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/mongoose/download/@types/mongoose-5.7.24.tgz",
|
||||
"integrity": "sha1-WSJJQ1oun+4CyellCv6sSAfU7wY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mongodb": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "13.13.9",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-13.13.9.tgz",
|
||||
@ -3022,6 +3056,44 @@
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npm.taobao.org/bl/download/bl-2.2.0.tgz",
|
||||
"integrity": "sha1-4aV0zfUo5AUwGbuACwQcCsiNpJM=",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz?cache=0&sync_timestamp=1562592096220&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fisarray%2Fdownload%2Fisarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.7.tgz?cache=0&sync_timestamp=1581624324274&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-2.3.7.tgz",
|
||||
"integrity": "sha1-Hsoc9xGu+BTAT2IlKjamL2yyO1c=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz",
|
||||
@ -3224,6 +3296,11 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"bson": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npm.taobao.org/bson/download/bson-1.1.4.tgz",
|
||||
"integrity": "sha1-92hw15nxW4VN/7fuMvCodHl/fok="
|
||||
},
|
||||
"buffer": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npm.taobao.org/buffer/download/buffer-4.9.2.tgz?cache=0&sync_timestamp=1588706716358&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbuffer%2Fdownload%2Fbuffer-4.9.2.tgz",
|
||||
@ -4328,6 +4405,11 @@
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"denque": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npm.taobao.org/denque/download/denque-1.4.1.tgz",
|
||||
"integrity": "sha1-Z0T/dkHBSMP4ppwwflEjXB9KN88="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
|
||||
@ -9431,6 +9513,11 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"kareem": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npm.taobao.org/kareem/download/kareem-2.3.1.tgz",
|
||||
"integrity": "sha1-3vEtnJQQF/q/sA+HOvlenJnhvoc="
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-6.0.3.tgz",
|
||||
@ -9731,6 +9818,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npm.taobao.org/memory-pager/download/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha1-2HUWVdItOEaCdByXLyw9bfo+ZrU=",
|
||||
"optional": true
|
||||
},
|
||||
"meow": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/meow/download/meow-5.0.0.tgz",
|
||||
@ -10199,6 +10292,49 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"mongodb": {
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npm.taobao.org/mongodb/download/mongodb-3.5.8.tgz?cache=0&sync_timestamp=1591967946651&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmongodb%2Fdownload%2Fmongodb-3.5.8.tgz",
|
||||
"integrity": "sha1-NFUIVkSbdF0UWHNzS/kiwS1rnKo=",
|
||||
"requires": {
|
||||
"bl": "^2.2.0",
|
||||
"bson": "^1.1.4",
|
||||
"denque": "^1.4.1",
|
||||
"require_optional": "^1.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"saslprep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "5.9.18",
|
||||
"resolved": "https://registry.npm.taobao.org/mongoose/download/mongoose-5.9.18.tgz",
|
||||
"integrity": "sha1-0DHtU7HmC6ShSR+1QZBorLHzPQA=",
|
||||
"requires": {
|
||||
"bson": "^1.1.4",
|
||||
"kareem": "2.3.1",
|
||||
"mongodb": "3.5.8",
|
||||
"mongoose-legacy-pluralize": "1.0.2",
|
||||
"mpath": "0.7.0",
|
||||
"mquery": "3.2.2",
|
||||
"ms": "2.1.2",
|
||||
"regexp-clone": "1.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sift": "7.0.1",
|
||||
"sliced": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
|
||||
"integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mongoose-legacy-pluralize": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npm.taobao.org/mongoose-legacy-pluralize/download/mongoose-legacy-pluralize-1.0.2.tgz",
|
||||
"integrity": "sha1-O6n5H6UHtRhtOZ+0CFS/8Y+1Y+Q="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
|
||||
@ -10224,6 +10360,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mpath": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npm.taobao.org/mpath/download/mpath-0.7.0.tgz",
|
||||
"integrity": "sha1-IOgQLidrcXCdbgfp+NTQ9kGvv7g="
|
||||
},
|
||||
"mquery": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npm.taobao.org/mquery/download/mquery-3.2.2.tgz",
|
||||
"integrity": "sha1-4Tg6OVGFLOI+N/YZqbNQ8fs2ZOc=",
|
||||
"requires": {
|
||||
"bluebird": "3.5.1",
|
||||
"debug": "3.1.0",
|
||||
"regexp-clone": "^1.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sliced": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz",
|
||||
"integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz",
|
||||
@ -11368,6 +11528,11 @@
|
||||
"safe-regex": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"regexp-clone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/regexp-clone/download/regexp-clone-1.0.0.tgz",
|
||||
"integrity": "sha1-Ii25Z2IydwViYLmSYmNUoEzpv2M="
|
||||
},
|
||||
"regexpp": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npm.taobao.org/regexpp/download/regexpp-3.1.0.tgz",
|
||||
@ -11488,6 +11653,22 @@
|
||||
"integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=",
|
||||
"dev": true
|
||||
},
|
||||
"require_optional": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/require_optional/download/require_optional-1.0.1.tgz",
|
||||
"integrity": "sha1-TPNaQkf2TKPfjC7yCMxJSxyo/C4=",
|
||||
"requires": {
|
||||
"resolve-from": "^2.0.0",
|
||||
"semver": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/resolve-from/download/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||
}
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.17.0.tgz",
|
||||
@ -11662,6 +11843,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"saslprep": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/saslprep/download/saslprep-1.0.3.tgz",
|
||||
"integrity": "sha1-TAL5RrVs9UKX40e6EJPnrKxM8iY=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"saxes": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/saxes/download/saxes-5.0.1.tgz",
|
||||
@ -11685,8 +11875,7 @@
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz",
|
||||
"integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=",
|
||||
"dev": true
|
||||
"integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc="
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
@ -11842,6 +12031,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"sift": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/sift/download/sift-7.0.1.tgz",
|
||||
"integrity": "sha1-R9YsULFZ0xbxNy+LU/nBDNIaSwg="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz",
|
||||
@ -11879,6 +12073,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/sliced/download/sliced-1.0.1.tgz",
|
||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npm.taobao.org/snapdragon/download/snapdragon-0.8.2.tgz",
|
||||
@ -12056,6 +12255,15 @@
|
||||
"integrity": "sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=",
|
||||
"dev": true
|
||||
},
|
||||
"sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npm.taobao.org/sparse-bitfield/download/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npm.taobao.org/spdx-correct/download/spdx-correct-3.1.1.tgz?cache=0&sync_timestamp=1590161967473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fspdx-correct%2Fdownload%2Fspdx-correct-3.1.1.tgz",
|
||||
@ -12926,6 +13134,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ts-mockito": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npm.taobao.org/ts-mockito/download/ts-mockito-2.6.1.tgz",
|
||||
"integrity": "sha1-vJ7iYZAzk05vrRxEVayltazjTnM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.5"
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "8.10.2",
|
||||
"resolved": "https://registry.npm.taobao.org/ts-node/download/ts-node-8.10.2.tgz",
|
||||
|
||||
@ -23,7 +23,9 @@
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^7.0.0",
|
||||
"@nestjs/core": "^7.0.0",
|
||||
"@nestjs/mongoose": "^7.0.1",
|
||||
"@nestjs/platform-express": "^7.0.0",
|
||||
"mongoose": "^5.9.18",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^6.5.4"
|
||||
@ -36,6 +38,7 @@
|
||||
"@nestjs/testing": "^7.0.0",
|
||||
"@types/express": "^4.17.3",
|
||||
"@types/jest": "25.2.3",
|
||||
"@types/mongoose": "^5.7.24",
|
||||
"@types/node": "^13.9.1",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "3.0.2",
|
||||
@ -49,6 +52,7 @@
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "26.1.0",
|
||||
"ts-loader": "^6.2.1",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"typescript": "^3.7.4"
|
||||
|
||||
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PostModule } from './post/post.module';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
|
||||
@Module({
|
||||
imports: [PostModule],
|
||||
imports: [PostModule, MongooseModule.forRoot('mongodb://localhost/blog')],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
||||
@ -3,6 +3,9 @@ import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// enable shutdown hooks explicitly.
|
||||
app.enableShutdownHooks();
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
4
src/post/create-post.dto.ts
Normal file
4
src/post/create-post.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class CreatePostDto {
|
||||
readonly title: string;
|
||||
readonly content: string;
|
||||
}
|
||||
43
src/post/data-initializer.service.ts
Normal file
43
src/post/data-initializer.service.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { PostService } from './post.service';
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
|
||||
@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.`));
|
||||
}
|
||||
// onApplicationBootstrap(): void {
|
||||
// this.data.forEach(d => {
|
||||
// this.save(d).subscribe(saved => console.log(saved));
|
||||
// });
|
||||
// }
|
||||
// onApplicationShutdown(signal?: string): void {
|
||||
// console.log(signal);
|
||||
// this.postModel.deleteMany({}).exec();
|
||||
// }
|
||||
}
|
||||
@ -1,17 +1,64 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PostController } from './post.controller';
|
||||
import { PostService } from './post.service';
|
||||
import { async } from 'rxjs/internal/scheduler/async';
|
||||
import { Post } from '../../dist/post/post.interface';
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
import { UpdatePostDto } from './update-post.dto';
|
||||
import { of, Observable } from 'rxjs';
|
||||
import { mock, verify, instance, anyString, anyNumber, when } from 'ts-mockito';
|
||||
import { Post } from './post.model';
|
||||
|
||||
//jest.mock('./post.service');
|
||||
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: [PostService],
|
||||
providers: [
|
||||
{
|
||||
provide: PostService,
|
||||
useClass: PostServiceFake,
|
||||
},
|
||||
],
|
||||
controllers: [PostController],
|
||||
}).compile();
|
||||
|
||||
@ -28,40 +75,159 @@ describe('Post Controller', () => {
|
||||
});
|
||||
|
||||
it('GET on /posts/1 should return one post ', done => {
|
||||
controller.getPostById(1).subscribe(data => {
|
||||
expect(data.id).toEqual(1);
|
||||
controller.getPostById('1').subscribe(data => {
|
||||
expect(data._id).toEqual('1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('POST on /posts should return all posts', async () => {
|
||||
const post:Post = {id:4, title:'test title', content:'test content'};
|
||||
const posts = await controller.createPost(post).toPromise();
|
||||
expect(posts.length).toBe(4);
|
||||
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:Post = {id:4, title:'test title', content:'test content'};
|
||||
controller.updatePost(1, post).subscribe(data => {
|
||||
expect(data.length).toBe(3);
|
||||
expect(data[0].title).toEqual('test title');
|
||||
expect(data[0].content).toEqual('test content');
|
||||
expect(data[0].updatedAt).toBeTruthy();
|
||||
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 => {
|
||||
controller.deletePostById('1').subscribe(data => {
|
||||
expect(data).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE on /posts/1001 should return false', done => {
|
||||
controller.deletePostById(1001).subscribe(data => {
|
||||
expect(data).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
describe('Post Controller(useValue fake object)', () => {
|
||||
let controller: PostController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: PostService,
|
||||
useValue: {
|
||||
findAll: () =>
|
||||
of<any[]>([
|
||||
{ _id: 'testid', title: 'title', content: 'test content' },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
controllers: [PostController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PostController>(PostController);
|
||||
});
|
||||
|
||||
it('should get all posts', async () => {
|
||||
const result = await controller.getAllPosts().toPromise();
|
||||
expect(result[0]._id).toEqual('testid');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Post Controller(useValue: jest mocked object)', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,35 +1,54 @@
|
||||
import { Controller, Query, Get, Param, Post, Body, HttpStatus, Put, Delete, ParseIntPipe } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Query,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Body,
|
||||
Put,
|
||||
Delete,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PostService } from './post.service';
|
||||
import { take, toArray } from 'rxjs/operators';
|
||||
import { Post as BlogPost } from './post.interface';
|
||||
import { Post as BlogPost } from './post.model';
|
||||
import { UpdatePostDto } from './update-post.dto';
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(private postService: PostService) {}
|
||||
|
||||
@Get('')
|
||||
getAllPosts(@Query('q') keyword?: string): Observable<BlogPost[]> {
|
||||
return this.postService.findAll(keyword).pipe(take(10), toArray());
|
||||
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', ParseIntPipe) id: number): Observable<BlogPost> {
|
||||
getPostById(@Param('id') id: string): Observable<BlogPost> {
|
||||
return this.postService.findById(id);
|
||||
}
|
||||
|
||||
@Post('')
|
||||
createPost(@Body() post: BlogPost):Observable<BlogPost[]> {
|
||||
return this.postService.save(post).pipe(toArray());
|
||||
createPost(@Body() post: CreatePostDto): Observable<BlogPost> {
|
||||
return this.postService.save(post);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
updatePost(@Param('id', ParseIntPipe) id: number, @Body() post: BlogPost): Observable<BlogPost[]> {
|
||||
return this.postService.update(id, post).pipe(toArray());
|
||||
updatePost(
|
||||
@Param('id') id: string,
|
||||
@Body() post: UpdatePostDto,
|
||||
): Observable<BlogPost> {
|
||||
return this.postService.update(id, post);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deletePostById(@Param('id', ParseIntPipe) id: number): Observable<boolean> {
|
||||
deletePostById(@Param('id') id: string): Observable<BlogPost> {
|
||||
return this.postService.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
export interface Post {
|
||||
id?: number;
|
||||
title:string;
|
||||
content: string;
|
||||
createdAt?: Date,
|
||||
updatedAt?: Date
|
||||
}
|
||||
18
src/post/post.model.ts
Normal file
18
src/post/post.model.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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);
|
||||
@ -1,9 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PostController } from './post.controller';
|
||||
import { PostService } from './post.service';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { PostSchema } from './post.model';
|
||||
import { DataInitializerService } from './data-initializer.service';
|
||||
|
||||
@Module({
|
||||
imports: [MongooseModule.forFeature([{ name: 'posts', schema: PostSchema }])],
|
||||
controllers: [PostController],
|
||||
providers: [PostService]
|
||||
providers: [PostService, DataInitializerService],
|
||||
})
|
||||
export class PostModule {}
|
||||
|
||||
@ -1,49 +1,87 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PostService } from './post.service';
|
||||
import { toArray, take, throttleTime } from 'rxjs/operators';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getModelToken } from '@nestjs/mongoose';
|
||||
import { Post } from './post.model';
|
||||
import { Model } from 'mongoose';
|
||||
|
||||
describe('PostService', () => {
|
||||
let service: PostService;
|
||||
let model: Model<Post>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PostService],
|
||||
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', done => {
|
||||
service
|
||||
.findAll()
|
||||
.pipe(take(3), toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(3),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
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);
|
||||
|
||||
it('getAllPosts with keyword should return 1 post', done => {
|
||||
service
|
||||
.findAll('Generate')
|
||||
.pipe(take(3), toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(1),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
const data = await service.findAll().toPromise();
|
||||
expect(data.length).toBe(3);
|
||||
});
|
||||
|
||||
it('getPostById with existing id should return 1 post', done => {
|
||||
service.findById(1).subscribe({
|
||||
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(1);
|
||||
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||
expect(data.title).toEqual('Generate a NestJS project');
|
||||
},
|
||||
error: error => console.log(error),
|
||||
@ -51,125 +89,56 @@ describe('PostService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('getPostById with none existing id should return empty', done => {
|
||||
service
|
||||
.findById(10001)
|
||||
.pipe(toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(0),
|
||||
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('save should increase the length of posts', done => {
|
||||
service
|
||||
.save({
|
||||
id: 4,
|
||||
title: 'test title',
|
||||
content: 'test content',
|
||||
})
|
||||
.pipe(take(4), toArray())
|
||||
.subscribe({
|
||||
next: data => {
|
||||
expect(data.length).toBe(4);
|
||||
expect(data[3].createdAt).toBeTruthy();
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
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('update should change the content of post', done => {
|
||||
service
|
||||
.update(1, {
|
||||
id: 1,
|
||||
title: 'test title',
|
||||
content: 'test content',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.pipe(take(4), toArray())
|
||||
.subscribe({
|
||||
next: data => {
|
||||
expect(data.length).toBe(3);
|
||||
expect(data[0].title).toBe('test title');
|
||||
expect(data[0].content).toBe('test content');
|
||||
expect(data[0].updatedAt).not.toBeNull();
|
||||
},
|
||||
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);
|
||||
|
||||
it('deleteById with existing id should return true', done => {
|
||||
service.deleteById(1).subscribe({
|
||||
service.deleteById('anystring').subscribe({
|
||||
next: data => expect(data).toBeTruthy,
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteById with none existing id should return false', done => {
|
||||
service.deleteById(10001).subscribe({
|
||||
next: data => expect(data).toBeFalsy,
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// see: https://stackoverflow.com/questions/62208107/how-test-a-rxjs-empty-operater-using-jest
|
||||
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
|
||||
describe('PostService(test for empty())', () => {
|
||||
let service: PostService;
|
||||
let testScheduler: TestScheduler;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PostService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PostService>(PostService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
// asserting the two objects are equal
|
||||
// e.g. using chai.
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// This test will actually run *synchronously*
|
||||
it('test complete for empty()', () => {
|
||||
testScheduler.run(helpers => {
|
||||
const { cold, expectObservable, expectSubscriptions } = helpers;
|
||||
// const e1 = cold('-a--b--c---|');
|
||||
// const subs = '^----------!';
|
||||
// const expected = '-a-----c---|';
|
||||
|
||||
// expectObservable(e1.pipe(throttleTime(3, testScheduler))).toBe(expected);
|
||||
// expectSubscriptions(e1.subscriptions).toBe(subs);
|
||||
expectObservable(service.findById(10001)).toBe('|');
|
||||
});
|
||||
});
|
||||
|
||||
it('test complete for empty():getPostById with none existing id', done => {
|
||||
let called = false;
|
||||
service.findById(10001).subscribe({
|
||||
next: data => {
|
||||
console.log(data);
|
||||
called = true;
|
||||
},
|
||||
error: error => {
|
||||
console.log(error);
|
||||
called = true;
|
||||
},
|
||||
complete: () => {
|
||||
console.log("calling complete");
|
||||
expect(called).toBeFalsy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,74 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Post } from './post.interface';
|
||||
import { of, from, Observable, empty, EMPTY } from 'rxjs';
|
||||
import { Post } from './post.model';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { CreatePostDto } from './create-post.dto';
|
||||
import { UpdatePostDto } from './update-post.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
private posts: Post[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Generate a NestJS project',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Create CRUD RESTful APIs',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Connect to MongoDB',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
constructor(@InjectModel('posts') private postModel: Model<Post>) {}
|
||||
|
||||
findAll(keyword?: string): Observable<Post> {
|
||||
findAll(keyword?: string, skip = 0, limit = 10): Observable<Post[]> {
|
||||
if (keyword) {
|
||||
return from(this.posts.filter(post => post.title.indexOf(keyword) >= 0));
|
||||
return from(
|
||||
this.postModel
|
||||
.find({ title: { $regex: '.*' + keyword + '.*' } })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.exec(),
|
||||
);
|
||||
} else {
|
||||
return from(
|
||||
this.postModel
|
||||
.find({})
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.exec(),
|
||||
);
|
||||
}
|
||||
|
||||
return from(this.posts);
|
||||
}
|
||||
|
||||
findById(id: number): Observable<Post> {
|
||||
const found = this.posts.find(post => post.id === id);
|
||||
if (found) {
|
||||
return of(found);
|
||||
}
|
||||
return EMPTY;
|
||||
findById(id: string): Observable<Post> {
|
||||
return from(this.postModel.findOne({ _id: id }).exec());
|
||||
}
|
||||
|
||||
save(data: Post): Observable<Post> {
|
||||
const post = { ...data, id: this.posts.length + 1, createdAt: new Date() };
|
||||
this.posts = [...this.posts, post];
|
||||
return from(this.posts);
|
||||
save(data: CreatePostDto): Observable<Post> {
|
||||
const createPost = this.postModel.create({ ...data });
|
||||
return from(createPost);
|
||||
}
|
||||
|
||||
update(id: number, data: Post): Observable<Post> {
|
||||
this.posts = this.posts.map(post => {
|
||||
if (id === post.id) {
|
||||
post.title = data.title;
|
||||
post.content = data.content;
|
||||
post.updatedAt = new Date();
|
||||
}
|
||||
return post;
|
||||
});
|
||||
return from(this.posts);
|
||||
update(id: string, data: UpdatePostDto): Observable<Post> {
|
||||
return from(this.postModel.findOneAndUpdate({ _id: id }, data).exec());
|
||||
}
|
||||
|
||||
deleteById(id: number): Observable<boolean> {
|
||||
const idx: number = this.posts.findIndex(post => post.id === id);
|
||||
if (idx >= 0) {
|
||||
// this.posts.splice(idx, 1);
|
||||
this.posts = [
|
||||
...this.posts.slice(0, idx),
|
||||
...this.posts.slice(idx + 1),
|
||||
];
|
||||
return of(true);
|
||||
}
|
||||
return of(false);
|
||||
deleteById(id: string): Observable<Post> {
|
||||
return from(this.postModel.findOneAndDelete({ _id: id }).exec());
|
||||
}
|
||||
|
||||
deleteAll(): Observable<any> {
|
||||
return from(this.postModel.deleteMany({}).exec());
|
||||
}
|
||||
}
|
||||
|
||||
4
src/post/update-post.dto.ts
Normal file
4
src/post/update-post.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class UpdatePostDto {
|
||||
readonly title: string;
|
||||
readonly content: string;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user