Testing
Testing
Implementing unit and integration tests is a proven way to make sure that your application behaves the way you expect it to. In many frameworks this is often a cumbersome and complicated experience, but not in Basica. You are always free to test your own code any way you want to, and if you need to test the interactions with Basica we got you covered.
Let's take the following application as a starting example.
import { ILogger } from "@basica/core/logger"
import { IStartup } from "@basica/core"
export type Config = {
msg: string
}
export const createSample = (logger: ILogger, config: Config) => ({
start: async () => {
deps.logger.info(config.msg);
},
sum: (n1: number, n2: number) => n1 + n2,
}) satisfies IStartup & Record<string, unknown>;
import { loggerFactory, loggerConfigSchema } from "@basica/core/logger"
import { AppBuilder } from "@basica/core"
import { run } from "@basica/platform-node";
import { configure, envProvider } from "@basica/config"
import { z } from "zod"
import { createSample } from "./sample"
const config = configure(
envProvider(),
z.object({
logger: loggerConfigSchema,
sample: z.object({
msg: z.string()
})
})
)
const app = AppBuilder.registerDependencies((di) =>
di
.addSingleton("logger", () => loggerFactory(config.logger))
.addSingleton("sample", (deps) => createSample(deps.logger, config.sample)),
)
.configureLifecycle((b) => b
.addService("sample", (deps) => deps.sample)
)
.build()
run(app)
We are of course, free from any constraint when trying to test sample.ts. Using vitest we could write the following:
import { loggerFactory } from "@basica/core/logger"
import { expect, test, vi, beforeEach } from "vitest"
import { createSample, Config } from "./sample"
const logger = loggerFactory({ level: "silent" })
const config = { msg: "Hello" } satisfies Config
const sample = createSample(logger, config)
beforeEach(() => {
vi.clearAllMocks()
})
test("start", async () => {
const spy = vi.spyOn(logger, "info")
expect(spy).toHaveBeenCalledWith(config.msg)
})
test.each([
[2, 2, 4]
[1, -1, 0]
[1, -10, -9]
])("sum %d + %d = %d", (n1, n2, expected) => {
const result = sample.sum(n1, n2)
expect(result).toEqual(expected)
})
Testing With Basica
But what if we want to test the wiring with Basica? We can easily do that by first moving the application boostrap in its own file.
import { loggerFactory, loggerConfigSchema } from "@basica/core/logger"
import { AppBuilder } from "@basica/core"
import { configure, envProvider, ConfigProvider } from "@basica/config"
import { z } from "zod"
import { createSample } from "./sample"
const configSchema = z.object({
logger: loggerConfigSchema,
sample: z.object({
msg: z.string()
})
})
export type Config = z.infer<typeof configSchema>
export const getApp = (provider: ConfigProvider = envProvider()) => {
const config = configure(provider, configSchema);
return AppBuilder.registerDependencies((di) =>
di
.addSingleton("logger", () => loggerFactory(config.logger))
.addSingleton("sample", (deps) => createSample(deps.logger, config.sample)),
)
.configureLifecycle((b) => b
.addService("sample", (deps) => deps.sample)
)
.build()
}
import { getApp } from "./app"
import { run } from "@basica/platform-node";
const app = getApp()
run(app)
Now, we can call getApp in our tests, passing another config provider.
import { ConfigProvider } from "@basica/config"
import { Config } from "./app"
export const config = {
sample: {
msg: "Hello",
},
logger: {
level: "silent",
},
} satisfies Config
export const testConfigProvider = {
get: () => config,
} satisfies ConfigProvider
We are now able to check if the service starts and stops correctly. We can also use the other exposed properties to check if our service was called properly during the startup.
import { expect, test, vi, beforeEach } from "vitest"
import { getApp } from "./app"
import { config, testConfigProvider } from "./testConfig"
beforeEach(() => {
vi.clearAllMocks()
})
test("start/stop", async () => {
const app = getApp(testConfigProvider)
const started = await app.lifecycle.start();
const stopped = await app.lifecycle.stop();
expect(started).toBe(true)
expect(stopped).toBe(true)
})
test("sample called correctly", async () => {
const app = getApp(testConfigProvider)
// app exposes deps, healthchecks, services and entrypoints
const spy = vi.spyOn(app.services.sample, "start")
await app.lifecycle.start();
expect(spy).toHaveBeenCalledWith(config.msg)
})
Healthchecks/services/entrypoints registered through a plugin are typed on the App as long as the plugin advertises its registrations (all official plugins do, see Writing plugins for more info).
import { lifecyclePlugin } from "@basica/fastify"
//...
export const getApp = (provider: ConfigProvider = envProvider()) => {
// ...
return AppBuilder.registerDependencies()
.configureLifecycle((b) => b
.with(lifecyclePlugin, (b) =>
b.addFastifyEntrypoint("http", (b) => {
//...
})
)
)
.build()
}
//...
const app = getApp(testConfigProvider)
type _ = typeof app.entrypoints.http // FastifyEntrypoint
const { fastify } = app.entrypoints.http
//...
test("GET /hello - 200" async () => {
const response = await fastify.inject("/hello")
expect(response.statusCode).toEqual(200)
})
//...