From b773eb1042ebb386b7253e70444fa0dcd028622f Mon Sep 17 00:00:00 2001 From: g0maa Date: Sat, 19 Oct 2024 09:56:59 +0300 Subject: [PATCH 1/4] feat: templates of different test types implemented --- apps/api/package.json | 1 + apps/api/src/app.resolver.spec.ts | 29 +++++++------- apps/api/src/app.service.spec.ts | 47 +++++++++++++++++++++++ apps/api/test/TestManager.ts | 63 +++++++++++++++++++++++++++++++ apps/api/test/app.e2e-spec.ts | 18 ++++----- apps/api/test/jest-e2e.json | 4 +- apps/api/test/setup.ts | 23 +++++++++++ apps/api/test/teardown.ts | 23 +++++++++++ pnpm-lock.yaml | 13 ++++--- 9 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 apps/api/src/app.service.spec.ts create mode 100644 apps/api/test/TestManager.ts create mode 100644 apps/api/test/setup.ts create mode 100644 apps/api/test/teardown.ts diff --git a/apps/api/package.json b/apps/api/package.json index bac4595..f57d83b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -40,6 +40,7 @@ "drizzle-orm": "^0.33.0", "express-session": "^1.18.0", "graphql": "^16.9.0", + "graphql-tag": "^2.12.6", "passport": "^0.7.0", "passport-local": "^1.0.0", "pg": "^8.13.0", diff --git a/apps/api/src/app.resolver.spec.ts b/apps/api/src/app.resolver.spec.ts index 6696b97..c801772 100644 --- a/apps/api/src/app.resolver.spec.ts +++ b/apps/api/src/app.resolver.spec.ts @@ -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!"); }); }); diff --git a/apps/api/src/app.service.spec.ts b/apps/api/src/app.service.spec.ts new file mode 100644 index 0000000..178a2db --- /dev/null +++ b/apps/api/src/app.service.spec.ts @@ -0,0 +1,47 @@ +import { ContextIdFactory } from "@nestjs/core"; +import { Test, TestingModule } from "@nestjs/testing"; +import { TestManager } from "../test/TestManager"; +import { AppResolver } from "./app.resolver"; +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(() => []), + }, + }, + }, + }, + }, + ], + }).compile(); + + drizzleService = app.get(DrizzleService); + appService = app.get(AppService); + }); + + afterAll(async () => {}); + + it('should return "Hello World!"', async () => { + const spy = jest.spyOn(drizzleService.db.query.users, "findMany"); + + const result = appService.testDb(); + expect(spy).toHaveBeenCalled(); + expect(result).toBe("[]"); + }); +}); diff --git a/apps/api/test/TestManager.ts b/apps/api/test/TestManager.ts new file mode 100644 index 0000000..eddc832 --- /dev/null +++ b/apps/api/test/TestManager.ts @@ -0,0 +1,63 @@ +import { INestApplication, ValidationPipe } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +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; + + // TODO: Find a way to abstract this logic, found in main.ts too. + async beforeAll(): Promise { + 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("SESSION_SECRET"), + resave: false, + saveUninitialized: false, + cookie: { + maxAge: configService.getOrThrow("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(). +} diff --git a/apps/api/test/app.e2e-spec.ts b/apps/api/test/app.e2e-spec.ts index 30fc4f6..efc2bde 100644 --- a/apps/api/test/app.e2e-spec.ts +++ b/apps/api/test/app.e2e-spec.ts @@ -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", () => { diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json index 055b528..b0fde81 100644 --- a/apps/api/test/jest-e2e.json +++ b/apps/api/test/jest-e2e.json @@ -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": { diff --git a/apps/api/test/setup.ts b/apps/api/test/setup.ts new file mode 100644 index 0000000..69b3a06 --- /dev/null +++ b/apps/api/test/setup.ts @@ -0,0 +1,23 @@ +import "tsconfig-paths/register"; + +import { createClient } from "redis"; +import { TestManager } from "./TestManager"; + +export default async (): Promise => { + 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."); +}; diff --git a/apps/api/test/teardown.ts b/apps/api/test/teardown.ts new file mode 100644 index 0000000..8dc18d8 --- /dev/null +++ b/apps/api/test/teardown.ts @@ -0,0 +1,23 @@ +import "tsconfig-paths/register"; + +import { createClient } from "redis"; +import { TestManager } from "./TestManager"; + +export default async (): Promise => { + console.log("# Started Jest globalTeardown."); + 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 globalTeardown."); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f95f19a..60373bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: graphql: specifier: ^16.9.0 version: 16.9.0 + graphql-tag: + specifier: ^2.12.6 + version: 2.12.6(graphql@16.9.0) passport: specifier: ^0.7.0 version: 0.7.0 @@ -6169,14 +6172,14 @@ snapshots: dependencies: '@graphql-tools/utils': 10.2.3(graphql@16.9.0) graphql: 16.9.0 - tslib: 2.6.3 + tslib: 2.7.0 '@graphql-tools/schema@10.0.4(graphql@16.9.0)': dependencies: '@graphql-tools/merge': 9.0.4(graphql@16.9.0) '@graphql-tools/utils': 10.2.3(graphql@16.9.0) graphql: 16.9.0 - tslib: 2.6.3 + tslib: 2.7.0 value-or-promise: 1.0.12 '@graphql-tools/schema@9.0.19(graphql@16.9.0)': @@ -6193,7 +6196,7 @@ snapshots: cross-inspect: 1.0.0 dset: 3.1.4 graphql: 16.9.0 - tslib: 2.6.3 + tslib: 2.7.0 '@graphql-tools/utils@9.2.1(graphql@16.9.0)': dependencies: @@ -7876,7 +7879,7 @@ snapshots: cross-inspect@1.0.0: dependencies: - tslib: 2.6.3 + tslib: 2.7.0 cross-spawn@7.0.3: dependencies: @@ -8468,7 +8471,7 @@ snapshots: graphql-tag@2.12.6(graphql@16.9.0): dependencies: graphql: 16.9.0 - tslib: 2.6.3 + tslib: 2.7.0 graphql-ws@5.16.0(graphql@16.9.0): dependencies: From 08d4a1b3bb5385015985e280e3ac66db37b0f80f Mon Sep 17 00:00:00 2001 From: g0maa Date: Sat, 19 Oct 2024 10:53:44 +0300 Subject: [PATCH 2/4] feat: working unit test --- apps/api/schema.graphql | 10 +++--- apps/api/src/app.resolver.ts | 5 +-- apps/api/src/app.service.spec.ts | 6 ++-- apps/api/src/app.service.ts | 5 +-- .../src/modules/users/entities/user.entity.ts | 32 +++++++++---------- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/apps/api/schema.graphql b/apps/api/schema.graphql index 1a4e2db..5411eb5 100644 --- a/apps/api/schema.graphql +++ b/apps/api/schema.graphql @@ -9,14 +9,14 @@ 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! @@ -24,7 +24,7 @@ type User { google_id: String id: Int! name: String! - profileImage: String - updatedAt: String! + profile_image: String + updated_at: String username: String! } \ No newline at end of file diff --git a/apps/api/src/app.resolver.ts b/apps/api/src/app.resolver.ts index e716f92..2c50c0e 100644 --- a/apps/api/src/app.resolver.ts +++ b/apps/api/src/app.resolver.ts @@ -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 { @@ -12,8 +13,8 @@ export class AppResolver { return this.appService.getHello(); } - @Query(() => String) - async db(): Promise { + @Query(() => [User]) + async db(): Promise { return this.appService.testDb(); } } diff --git a/apps/api/src/app.service.spec.ts b/apps/api/src/app.service.spec.ts index 178a2db..e81fe50 100644 --- a/apps/api/src/app.service.spec.ts +++ b/apps/api/src/app.service.spec.ts @@ -22,7 +22,7 @@ describe("[GraphQL] [UnitTesting] AppService", () => { db: { query: { users: { - findMany: jest.fn(() => []), + findMany: jest.fn().mockReturnValue([]), }, }, }, @@ -40,8 +40,8 @@ describe("[GraphQL] [UnitTesting] AppService", () => { it('should return "Hello World!"', async () => { const spy = jest.spyOn(drizzleService.db.query.users, "findMany"); - const result = appService.testDb(); + const result = await appService.testDb(); expect(spy).toHaveBeenCalled(); - expect(result).toBe("[]"); + expect(result).toStrictEqual([]); }); }); diff --git a/apps/api/src/app.service.ts b/apps/api/src/app.service.ts index fc7ea9a..49aeba2 100644 --- a/apps/api/src/app.service.ts +++ b/apps/api/src/app.service.ts @@ -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 { @@ -9,8 +10,8 @@ export class AppService { return "Hello World!"; } - async testDb(): Promise { + async testDb(): Promise { const users = await this.drizzleService.db.query.users.findMany(); - return `${users}`; + return users; } } diff --git a/apps/api/src/modules/users/entities/user.entity.ts b/apps/api/src/modules/users/entities/user.entity.ts index b1ca4f9..856f04b 100644 --- a/apps/api/src/modules/users/entities/user.entity.ts +++ b/apps/api/src/modules/users/entities/user.entity.ts @@ -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; } From 628abc45ca1a943b05066b20018c26a6f773188d Mon Sep 17 00:00:00 2001 From: g0maa Date: Sat, 19 Oct 2024 11:01:40 +0300 Subject: [PATCH 3/4] fix(ci): getting stuck on e2e tests --- apps/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/package.json b/apps/api/package.json index f57d83b..5122968 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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", From 22aa7e753bbe41334e67c799f7bfc610aaa1988d Mon Sep 17 00:00:00 2001 From: g0maa Date: Fri, 8 Nov 2024 11:02:20 +0200 Subject: [PATCH 4/4] fix(test): minor fixes --- apps/api/src/app.service.spec.ts | 3 --- apps/api/test/TestManager.ts | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/api/src/app.service.spec.ts b/apps/api/src/app.service.spec.ts index e81fe50..31df3f8 100644 --- a/apps/api/src/app.service.spec.ts +++ b/apps/api/src/app.service.spec.ts @@ -1,7 +1,4 @@ -import { ContextIdFactory } from "@nestjs/core"; import { Test, TestingModule } from "@nestjs/testing"; -import { TestManager } from "../test/TestManager"; -import { AppResolver } from "./app.resolver"; import { AppService } from "./app.service"; import { DrizzleService } from "./drizzle/drizzle.service"; diff --git a/apps/api/test/TestManager.ts b/apps/api/test/TestManager.ts index eddc832..2ff992d 100644 --- a/apps/api/test/TestManager.ts +++ b/apps/api/test/TestManager.ts @@ -1,5 +1,6 @@ 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"; @@ -10,7 +11,7 @@ import { AppModule } from "../src/app.module"; export class TestManager { // biome-ignore lint/suspicious/noExplicitAny: it is any. public httpServer: any; - public app: INestApplication; + public app: INestApplication; // TODO: Find a way to abstract this logic, found in main.ts too. async beforeAll(): Promise {