본문 바로가기
Web/Etc

[Nest] Unit Testing and E2E Testing

by llHoYall 2020. 10. 3.

In this time, let's find out the testing method of Nest.

I'll use the server that I made in the previous posting to test.

2020/10/02 - [Web/Etc] - [Nest] Create a simple backend server

 

Nest provides us with a whole testing environment consisting of Jest and SuperTest.

Usage

$ yarn test

To run tests only once.

$ yarn test:watch

To run tests with watch mode.

$ yarn test:cov

To run tests with coverage reports.

$ yarn test:e2e

To run tests with E2E mode.

Unit Test: Service

The default test file looks like this.

Open the contact.service.spec.ts file.

import { Test, TestingModule } from '@nestjs/testing';
import { ContactService } from './contact.service';

describe('ContactService', () => {
  let service: ContactService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ContactService],
    }).compile();

    service = module.get<ContactService>(ContactService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

We made services create, getAll, getOneWithId, getOneWithName, updateAll, updateOneWithID, deleteOneWithID.

Now, let's add tests for this.

Tests about Create

describe('Tests about Create', () => {
  it('creates and adds a contact', () => {
    const contact: CreateContactDto = {
      name: 'hoya',
      age: 18,
      email: ['abc@example.com'],
    };

    service.create(contact);
    const result = service.getOneWithName(contact.name);
    expect(result).toMatchObject(contact);
  });
});

I wrote the test code like above. The code is in the nested in the default describe.

I created contact and checked whether it is in the DB.

You can make your own test code.

Tests about Read

describe('Tests about Read', () => {
    beforeEach(async () => {
      service.create({
        name: 'hoya',
        age: 18,
        email: ['abc@example.com'],
      });
      service.create({
        name: 'park',
        age: 30,
        email: ['def@example.com'],
      });
    });

    it('get all of the contact', () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    });

    it('get one of the contact with ID', () => {
      const result = service.getOneWithID(1);
      expect(result).toHaveProperty('name', 'hoya');
    });

    it('get all of the contact with name', () => {
      const result = service.getOneWithName('park');
      expect(result).toHaveProperty('name', 'park');
    });
  });

For this test, I created the contact before each test.

I checked getAll service whether the result is an instance of an array.

And I checked the getOneWithID and getOneWithName service with the name property.

Tests about Update

describe('Tests about Update', () => {
  beforeEach(async () => {
    service.create({
      name: 'hoya',
      age: 18,
      email: ['abc@example.com'],
    });
    service.create({
      name: 'park',
      age: 30,
      email: ['def@example.com'],
    });
  });

  it('update all contact', () => {
    service.updateAll([
      { name: 'kim', age: 23, email: ['ghi@example.com'] },
      {
        name: 'lee',
        age: 37,
        email: ['jkl@example.com'],
      },
    ]);
    const result = service.getOneWithName('kim');
    expect(result).toHaveProperty('name', 'kim');
  });

  it('update one contact with ID', () => {
    service.updateOneWithID(1, { name: 'kim' });
    const result = service.getOneWithName('kim');
    expect(result).toHaveProperty('name', 'kim');
  });
});

It is the same as before.

I made tests for updating the contact with updateAll and updateOneWithID service.

Tests about Delete

cribe('Tests about Delete', () => {
  beforeEach(async () => {
    service.create({
      name: 'hoya',
      age: 18,
      email: ['abc@example.com'],
    });
    service.create({
      name: 'park',
      age: 30,
      email: ['def@example.com'],
    });
  });

  it('deletes one contact with ID', () => {
    service.deleteOneWithID(2);
    const result = service.getAll();
    expect(result).toHaveLength(1);
  });
});

I made a test for deleting with deleteOneWithID service.

At first, I registered two contacts and deleted one, so remained length of DB is 1.

Unit Test: Controller

Now, let's made tests for the controller.

In this time, I tested the failure case.

This test allows us to check that we have properly handled the error.

 

Open the contact.controller.spec.ts file.

The default test is as below.

import { Test, TestingModule } from '@nestjs/testing';
import { ContactController } from './contact.controller';
import { ContactService } from './contact.service';

describe('ContactController', () => {
  let controller: ContactController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [ContactController],
      providers: [ContactService],
    }).compile();

    controller = module.get<ContactController>(ContactController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });
});

We made a create, getAll, search, getOneWithID, updateAll, patch, delete function.

Let's test this!

Test about the GET method

There is no error in the POST method, so let's test the GET method first.

describe('Test about GET method', () => {
  it('should return error, if there is no contact with given ID', () => {
    try {
      controller.getOneWithID(1);
    } catch (e) {
      expect(e).toBeInstanceOf(NotFoundException);
    }
  });

  it('should return error, if there is no contact with given name', () => {
    try {
      controller.search('hoya');
    } catch (e) {
      expect(e.message).toEqual('Not found contact with name hoya');
    }
  });
});

 

If we try to get a contact that does not exist, it should return an error.

So I tested this.

Test about the PATCH method

There is no error in the PUT method, so I just tested the PATCH method.

describe('Test about PATCH method', () => {
  it('should return error, if there is no contact with given name', () => {
    try {
      controller.patch(1, { name: 'kim' });
    } catch (e) {
      expect(e).toBeInstanceOf(NotFoundException);
    }
  });
});

It is the same as the GET method case.

E2E (End-to-End) Test

The E2E test is from the user's point of view.

Let's open the app.e2e-spec.ts file.

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ transform: true }));
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

This is the default E2E test.

Access our server and check the response. That's it.

I added the ValidationPipe like our application, and it is the most important thing.

Only by doing this can we test it in the same way as our application.

In addition, I changed the beforeEach to beforeAll, because I want to keep the DB during the E2E test.

 

Let's make our own test.

describe('/contact', () => {
  it('/contact (POST)', () => {
    return request(app.getHttpServer())
      .post('/contact')
      .send({ name: 'hoya', email: ['abc@example.com'] })
      .expect(201)
      .expect({});
  });

  it('/contact (POST)', () => {
    return request(app.getHttpServer())
      .post('/contact')
      .send({ name: 123, email: ['def@example.com'] })
      .expect(400);
  });

  it('/contact (GET)', () => {
    return request(app.getHttpServer())
      .get('/contact')
      .expect(200)
      .expect([]);
  });

  it('/contact (PUT)', () => {
    return request(app.getHttpServer())
      .put('/contact')
      .send([
        { name: 'hoya', email: ['abc@example.com'] },
        { name: 'park', age: 30, email: ['def@example.com'] },
      ])
      .expect(200);
  });
});

The first part is only with the '/contact' path.

describe('/contact/:id', () => {
  it('/contact/1 (GET)', () => {
    return request(app.getHttpServer())
      .get('/contact/1')
      .expect(200);
  });

  it('/contact/9 (GET)', () => {
    return request(app.getHttpServer())
      .get('/contact/9')
      .expect(404);
  });

  it('/contact/1 (PATCH)', () => {
    return request(app.getHttpServer())
      .patch('/contact/1')
      .send({ name: 'kim' })
      .expect(200);
  });

  it('/contact/9 (PATCH)', () => {
    return request(app.getHttpServer())
      .patch('/contact/9')
      .send({ name: 'lee' })
      .expect(404);
  });

  it('/contact/1 (DELETE)', () => {
    return request(app.getHttpServer())
      .delete('/contact/1')
      .expect(200);
  });
});

This test suite is for testing with an ID like '/contact/:id'.

describe('/contact/search?', () => {
  it('/contact/search?name=park (GET)', () => {
    return request(app.getHttpServer())
      .get('/contact/search?name=park')
      .expect(200);
  });

  it('/contact/search?name=hoya (GET)', () => {
    return request(app.getHttpServer())
      .get('/contact/search?name=hoya')
      .expect(404);
  });
});

The final part is for testing with a query.

Conclusion

In my opinion, it is redundant to write tests for both service and controller.

All tests can be done in one place since they are strongly coupled.

In addition, the controller part is covered by the E2E test.

Make a robust and safety code with the unit test and E2E test.

'Web > Etc' 카테고리의 다른 글

[Firebase] Using Cloud Firestore as Database  (0) 2020.10.04
[Firebase] Firebase Authentication with React  (0) 2020.10.04
[Nest] Create a simple backend server  (0) 2020.10.02
[Web] Testing Rest API on VS Code  (0) 2020.09.30
[Gulp] Getting Started  (0) 2020.09.12

댓글