Testing Guide
DropBilko — Testing Guide
Status: Active | Tests implemented across apps/api/ and packages/core/
Version: 2.0
Last updated:Updated: 2026-03-0302
Source:Author: src/drop-app/vitest.config.ts,ALAI playwright.config.ts,Documentation package.json, Teamtests/
Table of Contents
- Testing Philosophy
- Testing Pyramid
- Tech Stack
- Actual Test
FrameworksFiles - Running Tests
- Test Configuration
- Test Setup & Mocking
- CI Integration
- Writing New Tests
- Coverage Reporting
- Testing Best Practices
- Debugging Tests
1. Testing Philosophy
Financial software has a higher correctness bar than typical web apps. Bilko's testing strategy prioritizes:
- Financial Logic Accuracy — VAT calculations, double-entry bookkeeping, currency conversion
- Data Integrity — No lost transactions, no balance discrepancies
- Regression Prevention — Once fixed, bugs stay fixed
- Fast Feedback — Tests run without a real database (mocked Prisma)
2. Testing Pyramid
/\
/E2E\ ← 10% (Critical user flows only)
/------\
/ Integ \ ← 20% (API endpoints, mocked DB)
/----------\
/ Unit \ ← 70% (Business logic, financial engine)
/--------------\
Distribution:
- 70% Unit Tests — Fast, isolated, test business logic in
@bilko/core - 20% Integration Tests — Test API endpoints with mocked Prisma client
- 10% E2E Tests — Full API integration test (single file in
tests/e2e/)
Coverage Targets by Module
@bilko/core accounting engine |
Double-entry errors = financial loss |
||
@bilko/core tax/VAT calculations |
Tax miscalculations = regulatory penalty | ||
multi-currency |
90% | N/A | FX errors = revenue leakage |
| Auth API | 85% | 90% | Security boundary |
| Invoice API | 80% | 90% | Core revenue feature |
| Expense API | 80% | 85% | Core cost tracking |
| Reports API | 75% | 80% | Regulatory output |
| Banking API | 75% | 75% | Complex matching logic |
| Multi-tenant isolation | N/A | 100% | GDPR + security critical |
Prerequisites3. Tech Stack
| Test Type | Framework | Purpose |
|---|---|---|
| Unit | Vitest | @bilko/core business logic (financial engine) |
| Integration | Vitest + Supertest | API endpoint testing with mocked Prisma |
| E2E | Vitest + Supertest | Full API integration (tests/e2e/api.test.ts) |
Why Vitest (not Jest)
- Native ESM support, Vite-based — faster than Jest
- Compatible with Turborepo workspace structure
- Watch mode with HMR
- Same API as Jest (easy migration)
Why Supertest (not Postman)
- Programmatic API testing within Vitest
- Works with Express app instance directly
- No running server required
4. Actual Test Files
apps/api/tests/ — Mock Suite (11 test files)
Tests with mocked Prisma — fast, no database required.
| File | Tests | What It Covers |
|---|---|---|
setup.ts |
— | Shared test setup: Prisma mock, JWT helpers, test data factories |
auth.test.ts |
11 | Register, login, refresh token, logout, GET /me — auth flows with mocked DB |
invoices.test.ts |
11 | List, create, get, update, status change (send/pay), delete invoice endpoints |
expenses.test.ts |
9 | List, create, get, update, approve/reject, delete expense endpoints |
contacts.test.ts |
9 | List, create, get, update, delete contact endpoints |
accounts.test.ts |
4 | List, get, create, delete chart-of-accounts endpoints |
banking.test.ts |
10 | Bank account management, transaction import, reconciliation endpoints |
reports.test.ts |
9 | P&L, balance sheet, VAT report, trial balance endpoints |
transactions.test.ts |
9 | Transaction ledger list, filter, and detail endpoints |
country.test.ts |
27 | Country plugin integration — tax rates for RS/BA/HR, invoice number formats |
chatbot.test.ts |
9 | Chatbot message, history, clear history — rate limit (429) test |
invoice-gl-reversal.test.ts |
6 | Invoice cancellation GL reversal — double-entry stays balanced |
new-endpoints.test.ts |
~10 | Receipt, VAT export PDF/XML, dashboard, security audit log, data export |
DockerTotal: is~120+ mock suite test cases
apps/api/tests/unit/ — Unit Suite (4 test files)
Service-layer tests. Mocked Prisma. No HTTP layer.
| File | Tests | What It Covers |
|---|---|---|
invoice-service-calculations.test.ts |
~15 | InvoiceService.createInvoice() arithmetic — line totals, VAT, FX |
two-factor.test.ts |
8 | TwoFactorService — enable, verify, disable TOTP with backup codes |
sef-submission.test.ts |
~10 | SefClient HTTP calls, InvoiceService.submitToSef() fire-and-forget |
vat-calculation.test.ts |
~20 | Pure VAT functions from country-rs, country-ba, country-hr packages |
apps/api/tests/e2e/ — E2E Suite (2 test files)
End-to-end workflow tests. Mocked services, no real DB required.
| File | Tests | What It Covers |
|---|---|---|
api.test.ts |
— | Full Express stack integration test (live server, no mocks) |
billing-flow.e2e.test.ts |
~8 | Full billing workflow: contact → invoice → send → pay → P&L → credit note |
apps/api/tests/integration/ — Real DB Suite (5 test files)
Requires docker-compose.test.yml PostgreSQL. Run with npm run test:integration.
| File | Tests | What It Covers |
|---|---|---|
auth.integration.test.ts |
4 | Registration + login + refresh against real DB |
invoice.integration.test.ts |
5 | Full invoice CRUD lifecycle against real DB |
credit-note-gl.integration.test.ts |
3 | Credit note GL entries balance in real DB |
report.integration.test.ts |
3 | Reports with real seeded transactions |
tenant-isolation.integration.test.ts |
5 | Cross-tenant data isolation (org A cannot read org B's data) |
Grand Total: ~390 tests across 27 test files
packages/core/tests/ — Unit Tests run(5 againsttest afiles)
| File | Tests | What |
|---|---|---|
|
20 | validateDoubleEntry
|
chart-of-accounts.test.ts |
32 | Chart of accounts operations: account creation, hierarchy, account types |
invoicing.test.ts |
22 | generateInvoiceNumber, calculateInvoiceTotals, validateLineItem |
multi-currency.test.ts |
24 | Currency conversion, exchange rate locking, precision handling |
tax.test.ts |
23 | VAT calculations for RS (20%), BA (17%), HR (25%), mixed rates, edge cases |
Total: 121 unit test cases
Grand Total: ~220 tests isacross set14 automaticallytest byfiles
tests/setup.tspostgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test
5. Running Tests
UnitRun + IntegrationAll Tests (Vitest)Turborepo)
# Recommended:From viaproject Makefileroot (handles— Dockerruns +all DATABASE_URLtests automatically)in makeall packages
npm run test
# DirectOr with turbo directly
npx turbo run (Dockertest
must
Run beAPI up)Mock Suite
cd src/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_testapps/api
npx vitest run
# Watch mode
(development)
cd src/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test npx vitest
# Run specificSpecific test file
npx vitest run tests/auth.test.ts
npx vitest run tests/chatbot.test.ts
# Specific test by name pattern
npx vitest run --reporter=verbose -t "POST /api/v1/auth/login"
Run API Unit Suite
cd src/drop-appapps/api
&&npx DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_testvitest run tests/unit/
# Specific service test
npx vitest run tests/unit/two-factor.test.ts
npx vitest run tests/unit/vat-calculation.test.ts
Run API E2E Suite
cd apps/api
npx vitest run tests/e2e/
# Full billing flow
npx vitest run tests/e2e/billing-flow.e2e.test.ts
Run Real DB Integration Suite
Requires Docker. Uses docker-compose.test.yml (port 5433).
# Start test database
docker-compose -f docker-compose.test.yml up -d
# Run integration tests
cd apps/api
npm run test:integration
# Or directly:
npx vitest run tests/integration/api-endpoints.
# Stop test database
docker-compose -f docker-compose.test.yml down
Run Core Unit Tests
cd packages/core
npx vitest run
# Watch mode
npx vitest
# Specific file
npx vitest run tests/tax.test.ts
# Run withWith coverage
cd src/drop-app && DATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_test
npx vitest run --coverage
Run in Verbose Mode
npx vitest run --reporter=verbose
Test Without Building
ConfigurationTests resolve workspace packages from TypeScript source (not dist/) via vitest.config.ts): aliases. No build step required before running tests.
Environment:6.
node- Test
pattern:Configurationapps/api-express/vitest.config.tsimport { defineConfig } from 'vitest/config' import path from 'path' export default defineConfig({ resolve: { alias: { // Resolves workspace packages from source — no dist/ build needed '@bilko/core': path.resolve(__dirname, '../../packages/core/src/index.ts'), '@bilko/domain-rs': path.resolve(__dirname, '../../packages/domain-rs/src/index.ts'), '@bilko/domain-ba': path.resolve(__dirname, '../../packages/domain-ba/src/index.ts'), '@bilko/domain-hr': path.resolve(__dirname, '../../packages/domain-hr/src/index.ts'), '@bilko/database': path.resolve(__dirname, '../../packages/database/src/index.ts'), }, }, test: { globals: false, environment: 'node', setupFiles: [], // Setup is imported per test file via tests/setup.ts include: ['tests/**/*.test.ts'], exclude: ['node_modules', 'dist'], }, })packages/core/vitest.config.ts Setupimportfile:{defineConfigtests/setup.ts(sets}from 'vitest/config' export default defineConfig({ test: { globals: true, root: '.',NODE_ENV=testsetsinclude: ['tests/**/*.test.ts'], }, })Key Configuration Differences
Setting DATABASE_URLapps/api-express)Pathpackages/corealias:@globals->./src—fileParallelism:falsetestsimportsrunexplicitsequentiallytrueto—avoiddescribe/it/expectPostgreSQLglobalraceconditionssetupFilesE2E TestsNone ( Playwright)per-fileimport of
)#./setupRunNone allE2EtestsaliasesnpxWorkspace playwrightpackagetestpath#aliasesRunNot specificneededprojectnpxplaywrightenvironmenttest --project=user-flows npx playwright test --project=full-flows npx playwright test --project=input-chaos # Run with UI mode npx playwright test --ui # View HTML report npx playwright show-reportnode
explicit)Configuration(playwright.config.ts:3-38):Serialnode
Default execution(1worker) to avoid rate limit conflictsAuto-starts dev server (npm run dev) if not runningHTML reporterTrace on first retry (CI mode: 2 retries)
7. Test
ArchitectureSetup & MockingSetupFile:apps/api/tests/setup.tsSetsNODE_ENV=testandDATABASE_URL=postgresql://drop:dev_only_not_a_secret@localhost:5433/drop_testfor all Vitest tests.Database Strategy (PostgreSQL — ADR-014)AllThe API integration tests use asharedmocked Prisma client — tests run without a real PostgreSQLtestdatabase.helperThe setup file:- Sets required environment variables (
tests/helpers/pg-test-db.ts):JWT- secrets, rate limit config)
Connects toMocks therealentiredrop_testsrc/lib/prismadatabasemoduleonwithport 5433vi.mock()Runs Drizzle schema push once before eachProvides testsuitedata factories and JWT token generatorsTruncates all tables between tests for isolationNo in-memory SQLite — full test/prod parity
// IntegrationImport setup (must be first import in test pattern (post-migration)file)
import {
getTestDb,createTestUser,
cleanupTestDbgenerateTestAccessToken,
generateTestRefreshToken,
TEST_USER_EMAIL,
TEST_USER_ID,
TEST_ORG_ID,
} from ".'./helpers/pg-test-db";setup'
Mock Prisma Pattern
// In test file — reference the mocked prisma
import { dbprisma } from "@/'../src/lib/db";prisma'
beforeAll(const mockPrisma = prisma as any
// Configure mock for a specific test
mockPrisma.user.findUnique.mockResolvedValue(testUser)
mockPrisma.user.findUnique.mockResolvedValue(null) // simulate not found
// Clear mocks between tests
beforeEach(() => {
vi.clearAllMocks()
})
Auth Token Generation
// Generate a valid JWT for test requests
const authToken = generateTestAccessToken()
// Generate token with specific role
const adminToken = generateTestAccessToken({ role: 'admin' })
const viewerToken = generateTestAccessToken({ role: 'viewer' })
// Generate refresh token (used in cookies)
const refreshToken = generateTestRefreshToken(TEST_USER_ID)
Service Mocking Pattern (for route tests)
// Mock entire service module
vi.mock('../src/services/invoice.service', () => {
const mockService = {
listInvoices: vi.fn(),
createInvoice: vi.fn(),
// ...
}
return { invoiceService: mockService }
})
import { invoiceService } from '../src/services/invoice.service'
const mockInvoiceService = invoiceService as any
// Configure per test
mockInvoiceService.createInvoice.mockResolvedValue(newInvoice)
// Verify calls
expect(mockInvoiceService.createInvoice).toHaveBeenCalledWith(
TEST_ORG_ID,
TEST_USER_ID,
expect.any(Object),
)
8. CI Integration
Tests run via GitHub Actions on every push and pull request. See .github/workflows/ci.yml.
CI Pipeline (4 parallel jobs)
# .github/workflows/ci.yml
on:
push:
branches: ['*']
pull_request:
branches: [main, staging]
jobs:
lint: # npx turbo run lint
type-check: # npx turbo run type-check (after prisma generate)
test: # npx turbo run test (unit + integration)
build: # npx turbo run build (needs lint, type-check, test)
Job Details
| Job | Command | Needs Prisma Generate | Blocks |
|---|---|---|---|
lint |
npx turbo run lint |
No | build |
type-check |
npx turbo run type-check |
Yes | build |
test |
npx turbo run test |
Yes | build |
build |
npx turbo run build |
Yes | Deploy |
Prisma Client Generation (required in CI)
- name: Generate Prisma Client
run: npx prisma generate --schema=packages/database/prisma/schema.prisma
This is required before type-check and test because the API source references @prisma/client types.
Concurrency
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Cancels in-flight CI runs on the same branch when new commits are pushed.
CI Gate Rules
| Gate | Condition | Blocks |
|---|---|---|
| Lint | Any lint error | PR merge + build |
| Type check | Any TypeScript error | PR merge + build |
| Tests | Any test failure | PR merge + build |
| Build | Lint/type-check/test all pass | Deploy |
9. Writing New Tests
API Integration Test — Pattern
// apps/api/tests/my-feature.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import request from 'supertest'
import { generateTestAccessToken, TEST_ORG_ID, TEST_USER_ID } from './setup'
import app from '../src/app'
// Mock the service
vi.mock('../src/services/my-feature.service', () => {
return {
myFeatureService: {
listItems: vi.fn(),
createItem: vi.fn(),
},
}
})
import { myFeatureService } from '../src/services/my-feature.service'
const mockService = myFeatureService as any
const authToken = generateTestAccessToken()
describe('GET /api/v1/my-feature', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns 200 with paginated list', async () => {
awaitmockService.listItems.mockResolvedValue({ getTestDb();data: [], total: 0, page: 1, pageSize: 20 });
afterEach(const res = await request(app)
.get('/api/v1/my-feature')
.set('Authorization', `Bearer ${authToken}`)
expect(res.status).toBe(200)
expect(res.body).toHaveProperty('data')
})
it('returns 401 without auth', async () => {
const res = await cleanupTestDb(request(app).get('/api/v1/my-feature');
expect(res.status).toBe(401)
});
})
Core Unit tests that test pure logic (validation, auth utilities) mock @/lib/db directly without a real DB connection.
Note: better-sqlite3 has been removed. There is no new Database(":memory:") pattern anymore.
E2E Test Helpers
File: tests/e2e/input-chaos.spec.ts:21-42
loginAsDemo(page) -- Mocks the /api/auth/me endpoint to bypass rate limiting in chaos tests. Returns a pre-authenticated user object.
CHAOS_STRINGS -- Dictionary of malicious/edge-case inputs used across E2E tests:
Empty, spaces, XSS, SQL injection, HTML injectionUnicode, RTL override, null bytesVery long strings (10K chars)Norwegian (aeoa), Bosnian (sdccz), Japanese charactersZalgo text, special characters
Writing Tests
Integration Test— Pattern (PostgreSQL)
// packages/core/tests/my-calculation.test.ts
import { describe, it, expect, beforeAll, afterEachexpect } from "vitest";'vitest'
import Decimal from 'decimal.js'
import { getTestDb, cleanupTestDbmyCalculation } from "'../helpers/pg-test-db";
// DATABASE_URL is set by tests/setup.ts — no manual config neededsrc/my-module/index'
describe("Feature"'myCalculation', () => {
beforeAll(asyncit('returns correct result for standard input', () => {
awaitconst getTestDb(result = myCalculation('100', '20');
//expect(result.eq(new Ensure schema is pushed to drop_testDecimal('20'))).toBe(true)
});
afterEach(asyncit('handles zero input', () => {
awaitconst cleanupTestDb(result = myCalculation('0', '20');
expect(result.eq(new Decimal('0'))).toBe(true)
})
it('throws for negative amounts', () => {
expect(() => myCalculation('-100', '20')).toThrow()
})
})
Naming Conventions
// Describe block: HTTP method + route OR function name
describe('POST /api/v1/invoices', () => { ... })
describe('calculateInvoiceTotals', () => { ... })
// TruncateIt allblock: tablesbe betweenspecific testsabout });scenario and expected outcome
it("does'returns something201 with DB"created invoice when data is valid', ...)
it('returns 400 when customerId is missing', ...)
it('calculates Serbian VAT at 20% on 100 RSD as 20 RSD', ...)
it('throws for negative amounts', ...)
10. Coverage Reporting
Generate Coverage (core unit tests)
cd packages/core
npx vitest run --coverage
Coverage Output Format
File | % Stmts | % Branch | % Funcs | % Lines
-------------------------|---------|----------|---------|--------
All files | XX.X | XX.X | XX.X | XX.X
accounting/index.ts | 95.0 | 90.0 | 100.0 | 95.0
invoicing/index.ts | 88.2 | 80.5 | 85.0 | 88.2
tax/index.ts | 92.0 | 88.0 | 100.0 | 92.0
Coverage Targets
| Category | Target | Rationale |
|---|---|---|
Financial logic (@bilko/core) |
>95% | Critical for correctness |
API routes (apps/api-express) |
>80% | Integration-level coverage |
| Services | >80% | Business logic layer |
11. Testing Best Practices
1. Arrange-Act-Assert (AAA)
it('creates invoice with correct totals', async () => {
// Arrange,ARRANGE
Act,const AssertnewInvoice against= realmockInvoice({ PostgreSQLstatus: 'draft' });
mockInvoiceService.createInvoice.mockResolvedValue(newInvoice)
// ACT
const res = await request(app)
.post('/api/v1/invoices')
.set('Authorization', `Bearer ${authToken}`)
.send({ customerId: CUSTOMER_UUID, items: [...] });
// ASSERT
expect(res.status).toBe(201)
expect(res.body.invoiceNumber).toBe('INV-2026-001')
})
Unit2. Test PatternBehavior, (pureNot logic, no DB)Implementation
import// {BAD describe,— it,tests expect,internal vimock }call fromorder
"vitest";expect(mockService.createInvoice.mock.calls[0][0]).toBe(orgId)
// MockGOOD @/lib/db entirely for pure unit— tests vi.mock("@/lib/db",observable (API behavior
expect(res.status).toBe(201)
expect(res.body).toHaveProperty('invoiceNumber')
=>
3. db:Always {Clear select:Mocks
beforeEach(() => {
it("validates input", (vi.clearAllMocks() => {
// Arrange, Act, Assert — no DB needed
});
});
E2E4. Test PatternEdge Cases for Financial Logic
Always test in core unit tests:
- Zero amounts (
calculateInvoiceTotals([])) - Empty input arrays
- Negative values (should throw)
- Decimal precision (
quantity: '3', unitPrice: '33.33') - Large amounts (overflow protection)
5. Never Hard-Code UUIDs in Tests
import// {BAD
test,const expectorgId }= from "@playwright/test";
test.describe.configure({ mode: "serial" });'123e4567-e89b-12d3-a456-426614174000'
// AvoidGOOD
rateconst limits
test.describe("Feature", ()orgId => {TEST_ORG_ID test("user flow", async ({ page }) => {
await page.goto("http://localhost:3000/login"); awaitfrom page.locator('input[type="email"]').fill("[email protected]");setup.ts, awaitgenerated page.locator('input[type="password"]').fill("demo1234");
await page.locator('button[type="submit"]').click();
await page.waitForURL("**/dashboard", { timeout: 15000 });
});
});per-run
Test12. ResultsDebugging Tests
CurrentRun
Verbose statusin (2026-03-03):Migrated to PostgreSQL. All tests run against drop_test on port 5433.
VitestMode
#npx Run:vitest makerun test
# All test files use PostgreSQL — no SQLite, no better-sqlite3--reporter=verbose
PlaywrightRun Single Test by Name
npx vitest run -t "returns 201 with created invoice"
Debug Mode (Node Inspector)
npx vitest --inspect-brk
# Then attach VS Code debugger or open chrome://inspect
Print Mock Call History
// In a test, add temporarily:
console.log(mockService.createInvoice.mock.calls)
VS Code Launch Configuration
{
"type": "node",
"request": "launch",
"name": "Debug Vitest",
"runtimeExecutable": "npx",
"runtimeArgs": ["vitest", "--inspect-brk", "--no-coverage"],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/apps/api-express"
}
Related Documents
3TesttestInventory:projects configured:user-flows,full-flows,TEST-INVENTORY.mdinput-chaosBackendinput-chaosdependsArchitecture:on../backend/BACKEND-ARCHITECTURE.md- CI/CD Pipeline: ../infrastructure/CI-CD.md
user-flowsTest
Last Updated: 2026-03-02
Status: Active — ~390 tests across 27 files (mock + unit + E2E + real-DB suites)
Coverage AreasTarget: