Skip to main content

Testing

Testing setup, strategies, and best practices for AsaHome Cloud.

Overview

AsaHome Cloud uses Jest for testing with the following test types:

TypeLocationPurpose
Unit Tests*.spec.tsTest individual functions/classes
Integration Tests*.spec.tsTest module interactions
E2E Teststest/*.e2e-spec.tsTest API endpoints

Running Tests

All Tests

npm test

Watch Mode

npm run test:watch

Coverage Report

npm run test:cov

E2E Tests

npm run test:e2e

Specific Test File

npm test -- auth.service.spec.ts

Specific Test Name

npm test -- --testNamePattern="should validate password"

Jest Configuration

Configuration in package.json:

{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s",
"!**/*.module.ts",
"!**/main.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

Unit Testing

Service Test Example

// src/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

describe('AuthService', () => {
let service: AuthService;
let usersService: UsersService;
let jwtService: JwtService;

const mockUsersService = {
findByEmail: jest.fn(),
findById: jest.fn(),
};

const mockJwtService = {
sign: jest.fn(() => 'mock-token'),
verify: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: JwtService, useValue: mockJwtService },
],
}).compile();

service = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('validateUser', () => {
it('should return user if credentials are valid', async () => {
const hashedPassword = await bcrypt.hash('password123', 10);
const mockUser = {
id: 'user-uuid',
email: 'test@example.com',
password: hashedPassword,
isActive: true,
};

mockUsersService.findByEmail.mockResolvedValue(mockUser);

const result = await service.validateUser('test@example.com', 'password123');

expect(result).toBeDefined();
expect(result.email).toBe('test@example.com');
expect(result).not.toHaveProperty('password');
});

it('should return null if password is invalid', async () => {
const hashedPassword = await bcrypt.hash('password123', 10);
const mockUser = {
id: 'user-uuid',
email: 'test@example.com',
password: hashedPassword,
isActive: true,
};

mockUsersService.findByEmail.mockResolvedValue(mockUser);

const result = await service.validateUser('test@example.com', 'wrongpassword');

expect(result).toBeNull();
});

it('should return null if user not found', async () => {
mockUsersService.findByEmail.mockResolvedValue(null);

const result = await service.validateUser('notfound@example.com', 'password');

expect(result).toBeNull();
});
});

describe('login', () => {
it('should return tokens for valid user', async () => {
const mockUser = {
id: 'user-uuid',
email: 'test@example.com',
role: 'user',
};

const result = await service.login(mockUser);

expect(result).toHaveProperty('accessToken');
expect(result).toHaveProperty('refreshToken');
expect(jwtService.sign).toHaveBeenCalled();
});
});
});

Controller Test Example

// src/devices/devices.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { DevicesController } from './devices.controller';
import { DevicesService } from './devices.service';

describe('DevicesController', () => {
let controller: DevicesController;
let service: DevicesService;

const mockDevicesService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
};

const mockUser = {
id: 'user-uuid',
email: 'test@example.com',
role: 'user',
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DevicesController],
providers: [
{ provide: DevicesService, useValue: mockDevicesService },
],
}).compile();

controller = module.get<DevicesController>(DevicesController);
service = module.get<DevicesService>(DevicesService);
});

describe('findAll', () => {
it('should return array of devices', async () => {
const mockDevices = [
{ id: 'device-1', name: 'Home Hub', isOnline: true },
{ id: 'device-2', name: 'Office Hub', isOnline: false },
];

mockDevicesService.findAll.mockResolvedValue(mockDevices);

const result = await controller.findAll(mockUser);

expect(result).toEqual(mockDevices);
expect(service.findAll).toHaveBeenCalledWith(mockUser.id);
});
});

describe('create', () => {
it('should create and return device', async () => {
const createDto = {
uuid: 'device-uuid',
deviceId: 'home-001',
name: 'Home Hub',
};

const mockDevice = {
id: 'created-id',
...createDto,
isOnline: false,
};

mockDevicesService.create.mockResolvedValue(mockDevice);

const result = await controller.create(createDto, mockUser);

expect(result).toEqual(mockDevice);
expect(service.create).toHaveBeenCalledWith(createDto, mockUser.id);
});
});
});

E2E Testing

Setup

// test/app.e2e-spec.ts
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.setGlobalPrefix('api/v1');
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
}));
await app.init();
});

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

describe('/api/v1/health (GET)', () => {
it('should return health status', () => {
return request(app.getHttpServer())
.get('/api/v1/health')
.expect(200)
.expect((res) => {
expect(res.body.status).toBe('ok');
expect(res.body.service).toBe('asahome-cloud');
});
});
});
});

Auth E2E Tests

// test/auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Auth (e2e)', () => {
let app: INestApplication;
let accessToken: string;
let refreshToken: string;

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

app = moduleFixture.createNestApplication();
app.setGlobalPrefix('api/v1');
await app.init();
});

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

describe('POST /auth/login', () => {
it('should login with valid credentials', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({
email: 'test@example.com',
password: 'password123',
})
.expect(201);

expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
expect(response.body.user).toHaveProperty('email', 'test@example.com');

accessToken = response.body.accessToken;
refreshToken = response.body.refreshToken;
});

it('should reject invalid credentials', async () => {
await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
})
.expect(401);
});

it('should validate email format', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({
email: 'invalid-email',
password: 'password123',
})
.expect(400);

expect(response.body.message).toContain('email must be an email');
});
});

describe('POST /auth/refresh', () => {
it('should refresh tokens', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.send({ refreshToken })
.expect(201);

expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
});
});

describe('GET /users/me', () => {
it('should return current user with valid token', async () => {
await request(app.getHttpServer())
.get('/api/v1/users/me')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('email');
});
});

it('should reject without token', async () => {
await request(app.getHttpServer())
.get('/api/v1/users/me')
.expect(401);
});
});
});

Testing Utilities

Mock Factories

// test/factories/user.factory.ts
import { User } from '../../src/users/entities/user.entity';

export const createMockUser = (overrides: Partial<User> = {}): User => ({
id: 'test-user-uuid',
email: 'test@example.com',
password: 'hashed-password',
firstName: 'Test',
lastName: 'User',
role: 'user',
isActive: true,
isEmailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});

export const createMockDevice = (overrides = {}) => ({
id: 'test-device-uuid',
uuid: 'device-uuid',
deviceId: 'home-001',
name: 'Test Device',
isOnline: true,
lastSeenAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});

Test Database

For integration tests, use a separate test database:

// test/setup.ts
import { DataSource } from 'typeorm';

export const testDataSource = new DataSource({
type: 'postgres',
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT || '5433'),
username: 'postgres',
password: 'postgres',
database: 'asahome_cloud_test',
entities: ['src/**/*.entity.ts'],
synchronize: true,
dropSchema: true,
});

Code Coverage

Minimum Coverage Requirements

MetricThreshold
Statements80%
Branches70%
Functions80%
Lines80%

Coverage Configuration

// package.json
{
"jest": {
"coverageThreshold": {
"global": {
"statements": 80,
"branches": 70,
"functions": 80,
"lines": 80
}
}
}
}

Generate Coverage Report

npm run test:cov

View the HTML report at coverage/lcov-report/index.html.

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: asahome_cloud_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run linter
run: npm run lint

- name: Run tests
run: npm run test:cov
env:
DB_HOST: localhost
DB_PORT: 5432

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info

Best Practices

  1. Test in isolation: Mock external dependencies
  2. Use factories: Create reusable test data builders
  3. Clean up: Reset state between tests
  4. Descriptive names: Use clear test descriptions
  5. AAA pattern: Arrange, Act, Assert
  6. Test edge cases: Include error scenarios
  7. Keep tests fast: Mock slow operations