Skip to content

Latest commit

 

History

History
230 lines (174 loc) · 6.51 KB

testing.md

File metadata and controls

230 lines (174 loc) · 6.51 KB

Testing Nestjs applications

In the previous posts, I have write a lot of testing codes to verify if our application is working as expected.

Nestjs provides integration with with Jest and Supertest out-of-the-box, and testing harness for unit testing and end-to-end (e2e) test.

Nestjs test harness

Like the Angular 's TestBed, Nestjs provide a similar Test facilities to assemble the Nestjs components for your testing codes.

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

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

Similar to the attributes in the @Module decorator, creatTestingModule defines the components that will be used in the tests.

We have demonstrated the methods to test a service in Nestjs applications, eg. in the post.service.spec.ts.

To isolate the dependencies in a service, there are several approaches.

  • Create a fake service to replace the real service, assemble it in the providers .

    providers: [
        {
            provide: UserService,
            useClass: FakeUserService
        }
    ],
  • Use a mock instance instead.

    providers: [
        provide: UserService,
        useValue: {
            send: jest.fn()
        }
    ],
    
  • For simple service providers, you can escape from the Nestjs harness, and create a simple fake dependent service, and use new to instantize your service in the setup hooks.

You can also import a module in Test.createTestingModule.

Test.createTestingModule({
        imports: []
       })

To replace some service in the imported modules, you can override it.

Test.createTestingModule({
        imports: []
       })
       .override(...)

Jest Tips and Tricks

Nestjs testing is heavily dependent on Jest framework. I have spent a lot of time to research testing all components in Nestjs applications.

Mocking external classes or functions

For example the mongoose.connect will require a real mongo server to connect, to mock the createConnection of mongoose.

Set up mocks before importing it.

jest.mock('mongoose', () => ({
    createConnection: jest.fn().mockImplementation(
        (uri:any, options:any)=>({} as any)
    ),
    Connection: jest.fn()
}))

//...
import { Connection, createConnection } from 'mongoose';
//

When a database provider is instantized, assert the createConnection is called.

it('connect is called', () => {
    //expect(conn).toBeDefined();
    //expect(createConnection).toHaveBeenCalledTimes(1); // it is 2 here. why?
    expect(createConnection).toHaveBeenCalledWith("mongodb://localhost/blog", {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        //see: https://mongoosejs.com/docs/deprecations.html#findandmodify
        useFindAndModify: false
    });
})

Mock parent classes through prototype

Have a look at the local auth guard tests.

Mock the method canActivate in the parent prototype.

describe('LocalAuthGuard', () => {
  let guard: LocalAuthGuard;
  beforeEach(() => {
    guard = new LocalAuthGuard();
  });
  it('should be defined', () => {
    expect(guard).toBeDefined();
  });
  it('should return true for `canActivate`', async () => {
    AuthGuard('local').prototype.canActivate = jest.fn(() =>
      Promise.resolve(true),
    );
    AuthGuard('local').prototype.logIn = jest.fn(() => Promise.resolve());
    expect(await guard.canActivate({} as ExecutionContext)).toBe(true);
  });

});

Extract the functionality into functions as possible

Let's have a look at the user.model.ts. Extract the pre save hook method and custom comparePassword method into standalone functions.

async function preSaveHook(next) {

  // Only run this function if password was modified
  if (!this.isModified('password')) return next();

  // Hash the password
  const password = await hash(this.password, 12);
  this.set('password', password);

  next();
}

UserSchema.pre<User>('save', preSaveHook);

function comparePasswordMethod(password: string): Observable<boolean> {
  return from(compare(password, this.password));
}

UserSchema.methods.comparePassword = comparePasswordMethod;

It is easy to test them like simple functions.

describe('preSaveHook', () => {
    test('should execute next middleware when password is not modified', async () => {
        const nextMock = jest.fn();
        const contextMock = {
            isModified: jest.fn()
        };
        contextMock.isModified.mockReturnValueOnce(false);
        await preSaveHook.call(contextMock, nextMock);
        expect(contextMock.isModified).toBeCalledWith('password');
        expect(nextMock).toBeCalledTimes(1);
    });

    test('should set password when password is modified', async () => {
        const nextMock = jest.fn();
        const contextMock = {
            isModified: jest.fn(),
            set: jest.fn(),
            password: '123456'
        };
        contextMock.isModified.mockReturnValueOnce(true);
        await preSaveHook.call(contextMock, nextMock);
        expect(contextMock.isModified).toBeCalledWith('password');
        expect(nextMock).toBeCalledTimes(1);
        expect(contextMock.set).toBeCalledTimes(1);
    });
});

End-to-end testing

Nestjs integrates supertest to send a request to the server side.

Use beforeAll and afterAll to start and stop the application, use request to send a http request to the server and assert the response result.

import * as request from 'supertest';
//...

describe('API endpoints testing (e2e)', () => {
    let app: INestApplication;
    beforeAll(async () => {
        const moduleFixture: TestingModule = await Test.createTestingModule({
            imports: [AppModule],
        }).compile();

        app = moduleFixture.createNestApplication();
        app.enableShutdownHooks();
        app.useGlobalPipes(new ValidationPipe());
        await app.init();
    });

    afterAll(async () => {
        await app.close();
    });

    // an example of using supertest reqruest.
    it('/posts (GET)', async () => {
        const res = await request(app.getHttpServer()).get('/posts').send();
        expect(res.status).toBe(200);
        expect(res.body.length).toEqual(3);
    });
}

More details for the complete e2e tests, check Nestjs 's test folder.