Skip to content

Commit

Permalink
Feat/testing approaches (#61)
Browse files Browse the repository at this point in the history
* feat: templates of different test types implemented

* feat: working unit test

* fix(ci): getting stuck on e2e tests

* fix(test): minor fixes
  • Loading branch information
G0maa authored Nov 8, 2024
1 parent 589608b commit 57ea70b
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 53 deletions.
20 changes: 20 additions & 0 deletions apps/api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e": "jest --config ./test/jest-e2e.json --forceExit",
"db:push": "drizzle-kit push --config=./drizzle.config.ts",
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config=./drizzle.config.ts",
Expand All @@ -41,6 +41,7 @@
"drizzle-orm": "^0.33.0",
"express-session": "^1.18.0",
"graphql": "^16.9.0",
"graphql-tag": "^2.12.6",
"nest-winston": "^1.9.7",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
Expand Down
10 changes: 5 additions & 5 deletions apps/api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@ type Mutation {
}

type Query {
db: String!
db: [User!]!
hello: String!
}

type User {
bio: String
coverImage: String
createdAt: String!
cover_image: String
created_at: String
deleted_at: String
dob: String!
email: String!
github_id: String
google_id: String
id: Int!
name: String!
profileImage: String
updatedAt: String!
profile_image: String
updated_at: String
username: String!
}
29 changes: 14 additions & 15 deletions apps/api/src/app.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { ContextIdFactory } from "@nestjs/core";
import { Test, TestingModule } from "@nestjs/testing";
import { AppModule } from "./app.module";
import { TestManager } from "../test/TestManager";
import { AppResolver } from "./app.resolver";
import { AppService } from "./app.service";
import { DrizzleModule } from "./drizzle/drizzle.module";

describe("AppService", () => {
describe("[GraphQL] [IntegrationTesting] AppResolver", () => {
let testManager = new TestManager();
let appResolver: AppResolver;

beforeEach(async () => {
// TODO: create proper test module / class.
const app: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
beforeAll(async () => {
await testManager.beforeAll();

// TODO: are there a better handling for this? @xUser5000
const contextId = ContextIdFactory.create();
jest.spyOn(ContextIdFactory, "getByRequest").mockImplementation(
() => contextId,
);

appResolver = await app.resolve(AppResolver, contextId);
appResolver = await testManager.app.resolve(AppResolver, contextId);
});

describe("root", () => {
it('should return "Hello World!"', () => {
expect(appResolver.hello()).toBe("Hello World!");
});
afterAll(async () => {
await testManager.afterAll();
});

it('should return "Hello World!"', async () => {
const result = appResolver.hello();
expect(result).toBe("Hello World!");
});
});
5 changes: 3 additions & 2 deletions apps/api/src/app.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Query, Resolver } from "@nestjs/graphql";
import { AppService } from "./app.service";
import { Public } from "./common/custom-decorators/public-endpoint";
import { User } from "./modules/users/entities/user.entity";

@Resolver()
export class AppResolver {
Expand All @@ -13,8 +14,8 @@ export class AppResolver {
}

@Public()
@Query(() => String)
async db(): Promise<string> {
@Query(() => [User])
async db(): Promise<User[]> {
return this.appService.testDb();
}
}
44 changes: 44 additions & 0 deletions apps/api/src/app.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AppService } from "./app.service";
import { DrizzleService } from "./drizzle/drizzle.service";

describe("[GraphQL] [UnitTesting] AppService", () => {
let appService: AppService;
let drizzleService: DrizzleService;

// Note: we *shouldn't* (?) need TestManager for unit tests.
beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
// Mocking can happen here (if appService has dependencies),
// or add specific mocks to each test case.
providers: [
AppService,
{
provide: DrizzleService,
useValue: {
db: {
query: {
users: {
findMany: jest.fn().mockReturnValue([]),
},
},
},
},
},
],
}).compile();

drizzleService = app.get<DrizzleService>(DrizzleService);
appService = app.get<AppService>(AppService);
});

afterAll(async () => {});

it('should return "Hello World!"', async () => {
const spy = jest.spyOn(drizzleService.db.query.users, "findMany");

const result = await appService.testDb();
expect(spy).toHaveBeenCalled();
expect(result).toStrictEqual([]);
});
});
5 changes: 3 additions & 2 deletions apps/api/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from "@nestjs/common";
import { DrizzleService } from "./drizzle/drizzle.service";
import { User } from "./modules/users/entities/user.entity";

@Injectable()
export class AppService {
Expand All @@ -9,8 +10,8 @@ export class AppService {
return "Hello World!";
}

async testDb(): Promise<string> {
async testDb(): Promise<User[]> {
const users = await this.drizzleService.db.query.users.findMany();
return `${users}`;
return users;
}
}
32 changes: 16 additions & 16 deletions apps/api/src/modules/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,27 @@ export class User {
@Field()
public dob: string;

@Field({ nullable: true })
public bio?: string;
@Field(() => String, { nullable: true })
public bio?: string | null;

@Field()
public createdAt: string;
@Field(() => String, { nullable: true })
public profile_image?: string;

@Field()
public updatedAt: string;
@Field(() => String, { nullable: true })
public cover_image?: string;

@Field({ nullable: true })
public profileImage?: string;
@Field(() => String, { nullable: true })
public google_id: string | null;

@Field({ nullable: true })
public coverImage?: string;
@Field(() => String, { nullable: true })
public github_id: string | null;

@Field({ nullable: true })
public google_id: string;
@Field(() => String, { nullable: true })
public deleted_at?: string | null;

@Field({ nullable: true })
public github_id: string;
@Field(() => String, { nullable: true })
public created_at?: string | null;

@Field({ nullable: true })
public deleted_at: string;
@Field(() => String, { nullable: true })
public updated_at?: string | null;
}
64 changes: 64 additions & 0 deletions apps/api/test/TestManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { INestApplication, ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import RedisStore from "connect-redis";
import * as session from "express-session";
import * as passport from "passport";
import { createClient } from "redis";
import { AppModule } from "../src/app.module";

export class TestManager {
// biome-ignore lint/suspicious/noExplicitAny: it is any.
public httpServer: any;
public app: INestApplication<NestExpressApplication>;

// TODO: Find a way to abstract this logic, found in main.ts too.
async beforeAll(): Promise<void> {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
this.app = moduleRef.createNestApplication();

const configService = this.app.get(ConfigService);

this.app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);

const redisClient = await createClient({
url: String(configService.getOrThrow("REDIS_URL")),
}).connect();

this.app.use(
session({
secret: configService.getOrThrow<string>("SESSION_SECRET"),
resave: false,
saveUninitialized: false,
cookie: {
maxAge: configService.getOrThrow<number>("COOKIE_MAX_AGE"),
httpOnly: true,
},
store: new RedisStore({
client: redisClient,
}),
}),
);

this.app.use(passport.initialize());
this.app.use(passport.session());
this.app.enableCors();

this.httpServer = this.app.getHttpServer();
await this.app.init();
}

async afterAll() {
await this.app.close();
}

// Helper functions can be added here if needed e.g. generateUser().
}
18 changes: 9 additions & 9 deletions apps/api/test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import * as request from "supertest";
import { AppModule } from "./../src/app.module";
import { TestManager } from "./TestManager";

describe("AppController (e2e)", () => {
describe("[GraphQL] [E2E] AppModule", () => {
const testManager = new TestManager();
let app: INestApplication;

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

app = moduleFixture.createNestApplication();
await app.init();
afterAll(async () => {
await testManager.afterAll();
});

it("/graphql helloworld", () => {
Expand Down
4 changes: 3 additions & 1 deletion apps/api/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"rootDir": "./../",
"globalSetup": "./test/setup.ts",
"globalTeardown": "./test/teardown.ts",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
Expand Down
23 changes: 23 additions & 0 deletions apps/api/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "tsconfig-paths/register";

import { createClient } from "redis";
import { TestManager } from "./TestManager";

export default async (): Promise<void> => {
console.log("# Started Jest globalSetup.");
const testManager = new TestManager();

await testManager.beforeAll();

await testManager.app.init();

// TODO: Apply Database migrations/seeders.

await testManager.app.close();

// Delete records in redis.
const client = createClient();
await client.connect();
await client.flushAll();
console.log("# Finished Jest globalSetup.");
};
Loading

0 comments on commit 57ea70b

Please sign in to comment.