Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing #55

Merged
merged 5 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .barrelsby.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"./src/constants",
"./src/dtos/in",
"./src/dtos/out",
"./src/dtos/common",
"./src/configs",
"./src/hooks",
"./src/interfaces",
Expand Down
23 changes: 6 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
paths-ignore:
- '.husky/**'
- '**.md'
- 'docs/**'
- 'package.json'

jobs:
Expand All @@ -15,35 +16,23 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [19.x]
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v3
name: Checkout repository

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-


- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'

- name: Install dependencies
run: yarn
run: yarn && yarn db:generate

- name: Test
run: yarn lint && yarn test
- name: Run unit tests
run: yarn test unit

- name: Build
run: yarn build
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

Using [fastify](https://www.fastify.io), this template includes:

- API Docs: `SwaggerUI`
- Input validation: `fluent-json-schema`
- ORM & migration tools: `Prisma`
- API Docs: `Swagger UI`
- Input definition: [`Typebox`](https://github.com/sinclairzx81/typebox)
- ORM: `Prisma`
- Deployment:
- Dockerfile & docker-compose files
- Script CI/CD in `.github/workflows`
Expand All @@ -16,14 +16,14 @@ Using [fastify](https://www.fastify.io), this template includes:

For applying conventional commits, refer [commitizen](https://github.com/commitizen/cz-cli).

## Prerequisites
## 1. Prerequisites

- `docker` v20.10.22
- `docker-compose` v1.29.2
- `node` v18.13.0
- `npm` 8.19.3

## Commands
## 2. Commands

Note: Fill in `.env` file (use template from `.env.example`) before starts.

Expand All @@ -40,8 +40,9 @@ Note: Fill in `.env` file (use template from `.env.example`) before starts.
- `yarn start:docker`: Run `docker-compose.dev.yml` file to set up local database
- `yarn clean:docker`: Remove local database instance include its data.
- `yarn clean:git`: Clean local branches which were merged on remote
- `yarn test <test_label>`: Run test with label `<test_label>`. For example: `yarn test auth` will run all tests in `auth.test.ts` or `auth.spec.ts` file.

## Project structure
## 3. Project structure

```py
📦prisma
Expand All @@ -53,10 +54,10 @@ Note: Fill in `.env` file (use template from `.env.example`) before starts.
┣ 📂constants # Constants and enums go here
┣ 📂dtos # Schema for input (from requests) & output (from responses)
┃ ┣ 📂in
┃ ┗ 📂out
┃ ┣ 📂out
┃ ┗ 📂common # Reusable schemas
┣ 📂handlers # Handlers, which are responsible for handling core business logic
┣ 📂interfaces # Interfaces
┣ 📂middlewares # Middlewares such as logging or verifying tokens
┣ 📂plugins # Plugin, in charge of organizing api routings & registering middleware
┣ 📂repositories # Datasource configurations & connections. Could have more than one datasource.
┣ 📂services # 3rd-party services or business logic services
Expand All @@ -66,7 +67,25 @@ Note: Fill in `.env` file (use template from `.env.example`) before starts.
┗ 📜index.ts # Program entry
```

## Project configurations
## 4. Appendix

This section contains some useful information for development.

### Testing

These are some best practices for testing and overall quality:

1. At the very least, write API (component) testing.
2. Include 3 parts in each test name
3. Structure tests by the AAA pattern
4. Ensure Node version is unified
5. Avoid global test fixtures and seeds, add data per-test
6. Check your test coverage, it helps to identify wrong test patterns
7. Refactor regularly using static analysis tools
8. Mock responses of external HTTP services
9. Test your middlewares in isolation
10. Specify a port in production, randomize in testing
11. Test the five possible outcomes

### Code linting & formating

Expand Down Expand Up @@ -132,15 +151,13 @@ yarn barrels

To avoid using many `..` in relative path, config path alias in `tsconfig.json`. See the guideline [here](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping).

## Git working culture
### Git working culture

- For every updates, DO NOT push directly to `master` branch. Create a new branch, commit, publish branch and create a pull request (PR) instead.
- A branch should have prefix `feat/` for a feature update, prefix `hotf/` for a hotfix, `improv/` for an improvement ...
- A PR should be small enough to review. To split a large PR, use [stacked PRs](https://blog.logrocket.com/using-stacked-pull-requests-in-github/).

## Helpful resources

### Prisma
### About Prisma ORM

- [Database schema](https://www.prisma.io/docs/concepts/components/prisma-schema)
- [Type mapping Prisma & PostgreSQL](https://www.prisma.io/docs/concepts/database-connectors/postgresql#type-mapping-between-postgresql-to-prisma-schema)
Expand Down
3 changes: 1 addition & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ version: "3.5"
services:
postgres:
image: postgres:15
container_name: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- 5432:5432
- 5433:5432
volumes:
- postgres_db:/var/lib/postgresql/data
restart: always
Expand Down
82 changes: 65 additions & 17 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
const { pathsToModuleNameMapper } = require('ts-jest');
// which contains the path mapping (ie the `compilerOptions.paths` option):
const { compilerOptions } = require('./tsconfig');
const fs = require('fs');
const path = require('path');

/** @type {import('@jest/types').Config.ProjectConfig} */
const generalConfig = {
/**
* @type {import('@jest/types').Config.ProjectConfig}
* Config used between test projects.
*/
const baseConfig = {
// Automatically clear mock calls and instances between every test
clearMocks: true,
coveragePathIgnorePatterns: ['index.ts', '/node_modules/'],
Expand All @@ -14,12 +19,65 @@ const generalConfig = {
transform: {
'\\.(ts)$': 'ts-jest'
},
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
transformIgnorePatterns: ['node_modules'],
moduleDirectories: ['node_modules', 'src'],
roots: ['<rootDir>'],
modulePaths: [compilerOptions.baseUrl], // <-- This will be set to 'baseUrl' value
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths /*, { prefix: '<rootDir>/' } */)
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths /*, { prefix: '<rootDir>/' } */),
modulePathIgnorePatterns: ['<rootDir>/dist/']
};

/**
* For easier debugging, create a test label for each test file.
* @param {string} baseDir
* @returns
*/
function createTestLabels(baseDir) {
// Read the contents of the directory synchronously
const filePaths = fs.readdirSync(baseDir).map((f) => path.join(baseDir, f));
const results = [];

while (filePaths.length > 0) {
const filePath = filePaths.pop();
const fileStat = fs.statSync(filePath); // Get the file's stats

if (fileStat.isDirectory()) {
const subFiles = fs.readdirSync(filePath);
filePaths.push(...subFiles.map((f) => path.join(filePath, f)));
continue;
}

// Check if the file is test file
let type;
if (filePath.endsWith('.spec.ts')) type = 'spec';
else if (filePath.endsWith('.test.ts')) type = 'test';
else continue;

const fileName = path.basename(filePath);
const label = fileName.slice(0, -8);

results.push({ label, type });
}

return results.map((r) => ({
...baseConfig,
displayName: r.label,
testMatch: [`**/${r.label}.${r.type}.ts`],
setupFilesAfterEnv: r.type === 'spec' ? ['<rootDir>/src/setupTest.ts'] : undefined
}));
}

const unitTestProject = {
...baseConfig,
displayName: 'unit',
testMatch: ['**/*.test.ts']
};

const integrationTestProject = {
...baseConfig,
displayName: 'integration',
testMatch: ['**/*.spec.ts'],
setupFilesAfterEnv: ['<rootDir>/src/setupTest.ts']
};

/** @type {import('jest').Config} */
Expand All @@ -32,19 +90,9 @@ module.exports = {
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
detectOpenHandles: true,

// Wait at most 5 seconds util all promises are resolved before exiting the test
openHandlesTimeout: 5000,
projects: [
{
...generalConfig,
displayName: 'unit-tests',
testMatch: ['**/*.test.ts']
},
{
...generalConfig,
displayName: 'integration-tests',
testMatch: ['**/*.spec.ts']
}
]
// Exit the test suite immediately upon the first failing test
bail: true,
projects: [unitTestProject, integrationTestProject, ...createTestLabels(compilerOptions.sourceRoot)]
};
65 changes: 34 additions & 31 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "tsc --project tsconfig.compile.json && tsc-alias",
"barrels": "barrelsby --config .barrelsby.json -q",
"start": "yarn barrels && cross-env NODE_ENV=development tsnd --ignore-watch node_modules --respawn --transpile-only --max-old-space-size=2048 -r tsconfig-paths/register src/index.ts",
"test": "jest --passWithNoTests",
"test": "jest --forceExit --selectProjects",
"db:migrate": "npx prisma migrate dev",
"db:reset": "npx prisma migrate reset --force",
"db:deploy": "npx prisma migrate deploy",
Expand All @@ -24,46 +24,49 @@
"clean:git": "git branch --merged >/tmp/merged-branches && nano /tmp/merged-branches && xargs git branch -D </tmp/merged-branches && git fetch --prune --all"
},
"dependencies": {
"@fastify/cookie": "^9.0.4",
"@fastify/cors": "^8.3.0",
"@fastify/helmet": "^11.0.0",
"@fastify/sensible": "^5.2.0",
"@fastify/swagger": "^8.8.0",
"@fastify/swagger-ui": "^1.9.3",
"@prisma/client": "^5.1.1",
"@sinclair/typebox": "^0.31.1",
"bcrypt": "^5.1.0",
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.5.0",
"@fastify/helmet": "^11.1.1",
"@fastify/sensible": "^5.5.0",
"@fastify/swagger": "^8.13.0",
"@fastify/swagger-ui": "^2.0.1",
"@prisma/client": "^5.7.1",
"@sinclair/typebox": "^0.32.5",
"bcrypt": "^5.1.1",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"envalid": "^7.3.1",
"fastify": "^4.21.0",
"jsonwebtoken": "^9.0.1",
"moment-timezone": "^0.5.43",
"prisma": "^5.1.1"
"envalid": "^8.0.0",
"fastify": "^4.25.2",
"jsonwebtoken": "^9.0.2",
"moment-timezone": "^0.5.44",
"prisma": "^5.7.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/jest": "^29.5.3",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.8",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"barrelsby": "^2.8.0",
"@faker-js/faker": "^8.3.1",
"@types/bcrypt": "^5.0.2",
"@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.10.7",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.18.0",
"@typescript-eslint/parser": "^6.18.0",
"barrelsby": "^2.8.1",
"cross-env": "^7.0.3",
"dotenv-cli": "^7.2.1",
"eslint": "^8.46.0",
"eslint-config-prettier": "^9.0.0",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.6.2",
"lint-staged": "^13.2.3",
"pino-pretty": "^10.2.0",
"prettier": "^3.0.1",
"jest": "^29.7.0",
"lint-staged": "^15.2.0",
"pino-pretty": "^10.3.1",
"prettier": "^3.1.1",
"supertest": "^6.3.3",
"ts-jest": "^29.1.1",
"ts-node-dev": "^2.0.0",
"tsc-alias": "^1.8.7",
"tsc-alias": "^1.8.8",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.6"
"typescript": "^5.3.3"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
Expand Down
6 changes: 3 additions & 3 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export function createServer(config: ServerConfig): FastifyInstance {
hook: 'onRequest'
} as FastifyCookieOptions);

// Swagger on production will be turned off in the future
if (envs.isDev) {
// Swagger on production should be turned off
if (!envs.isProd) {
app.register(import('@fastify/swagger'), swaggerConfig);
app.register(import('@fastify/swagger-ui'), swaggerUIConfig);
}
Expand All @@ -42,7 +42,7 @@ export function createServer(config: ServerConfig): FastifyInstance {
await app.ready();
if (!envs.isProd) {
app.swagger({ yaml: true });
app.log.info(`Swagger documentation is on http://${config.host}:${config.port}/docs`);
app.log.info(`Swagger documentation is on ${app.server.address()}/docs`);
}
};

Expand Down
Loading