The Codebricks Generator generates also Test Code. This comprises integration tests and the corresponding helpers and unit tests.
Setup:
For executing local integration test you need to set up a local Postgres database.
To connect to this database set the POSTGRES_URL
in the .env.testing
file.
Next we need to execute the migrations on this test database by running:
npm run test-integration-setup
Execute all integration tests running:
npm run test-integration
Execute a single integration tests running:
npm run test-integration-one %PATH_TO_TEST%
The purpose of the Command API integration test is to verify the "happy path" of the application, covering the flow:
From API Request to the Response to Event.
An example of an integration test can be found under: Task/tests/integration/useCases/write/AddTask/AddTaskApi.test.ts
1import { describe, it, before, after } from "mocha";2import { expect } from "chai";3import { APIGatewayProxyResult, Event, parseToDateTime } from "@codebricks/typebricks";4import { MockHelper } from "tests/integration/utils/MockHelper";5import { IntegrationTestHelper, TExpectedEvent } from "tests/integration/utils/IntegrationTestHelper";6import { AddTaskApiRequest, AddTaskApiResponseBody } from "useCases/write/AddTask/infrastructure/AddTaskApi";7import { handler } from "useCases/write/AddTask/infrastructure/AddTaskApiHandler";8import { ExpectedTaskAddedEvent } from "tests/integration/utils/expected/events/ExpectedTaskAddedEvent";910const aggregateId: string = '32b1ec06-adef-4516-b992-0af2eec32b5b';11const now: Date = new Date();12const request: AddTaskApiRequest = {13 title: '',14 description: '',15 assigneeId: '',16};17const expectedResponse: AddTaskApiResponseBody = {18 taskId: '',19};20const expectedEvents: TExpectedEvent[] = [21 ExpectedTaskAddedEvent.event({}),22];2324describe('POST add-task',25 /** @overwrite-protection-body false */26 async function() {27 before(async function() {28 MockHelper.mockSQSClient();29 MockHelper.stubUuidGenerator(aggregateId);30 MockHelper.stubClock(now);31 await IntegrationTestHelper.resetDB();32 });3334 after(function() {35 MockHelper.restore();36 });3738 it('produces TaskAddedEvent', async function() {39 const result: APIGatewayProxyResult = await handler(40 IntegrationTestHelper.toApiGatewayEvent(request)41 );42 expect(result.statusCode).equal(200);43 expect(JSON.parse(result.body, parseToDateTime)?.data).to.deep.equal(expectedResponse);44 await IntegrationTestHelper.expectEventsOnStream(expectedEvents);45 });46 });47
Key Concepts:
Given and Expectation Variables:
In the lines 10-22 the given state and expectations are defined.
The generator initializes these variables. The values have to be set manually, depending on the expected behavior of the system.
All those variables are protected so your changes will be retained during the next generator run.
Additional variables can be declared here and will be retained.
See GivenHelpers.
Before:
In lines 27-32 the setup for the tests is prepared.
Dependencies are mocked:
The database is resetted.
Optional: Database is seeded with Events and/or Readmodels.
After:
The after function is releasing the Mocks to avoid side effects between tests.
Test Functions:
Here the actual test is implemented.
This is the test flow:
request
variable.expectedResponse
variableexpectedEvent
in the event stream.The objective of the command policy integration test is to test the happy path trough the application:
From Processed Event to Event.
An example of an integration test can be found under: Task/tests/integration/useCases/write/CloseTask/ProcessUserDeleted.test.ts
1import { describe, it, before, after } from "mocha";2import { expect } from "chai";3import { Event } from "@codebricks/typebricks";4import { MockHelper } from "tests/integration/utils/MockHelper";5import { IntegrationTestHelper, TExpectedEvent } from "tests/integration/utils/IntegrationTestHelper";6import { UserDeletedEventMessage } from "shared/application/inboundEvents/UserDeletedEventMessage";7import { GivenUserDeletedEventMessage } from "tests/integration/utils/given/inboundEvents/GivenUserDeletedEventMessage";8import { GivenTaskAddedEvent } from "tests/integration/utils/given/events/GivenTaskAddedEvent";9import { ExpectedTaskClosedEvent } from "tests/integration/utils/expected/events/ExpectedTaskClosedEvent";1011const aggregateId: string = '9f4da7d1-961f-4ab6-9d81-79ad68da039b';12const now: Date = new Date();13const givenEvents: Event<any>[] = [14 GivenTaskAddedEvent.event({15 aggregateId: aggregateId,16 aggregateVersion: 1,17 }),18];19const processedEvent: UserDeletedEventMessage = GivenUserDeletedEventMessage.inboundEvent({20 streamName: 'Taskbricks.User',21 no: 1,22 aggregateId: '',23});24const expectedEvents: TExpectedEvent[] = [25 ExpectedTaskClosedEvent.event({}),26];2728describe('Processing UserDeleted',29 /** @overwrite-protection-body false */30 async function() {31 before(async function() {32 MockHelper.mockSQSClient();33 MockHelper.stubClock(now);34 await IntegrationTestHelper.resetDB();35 await IntegrationTestHelper.givenTaskAggregate(givenEvents);36 });3738 after(function() {39 MockHelper.restore();40 });4142 it('produces TaskClosedEvent', async function() {43 await IntegrationTestHelper.whenProcessingCloseTask(processedEvent);44 await IntegrationTestHelper.expectEventsOnStream(expectedEvents, givenEvents);45 });46 });47
Key Concepts:
Given and Expectation Variables:
In the lines 11-26 the given state and expectations are defined.
The generator initializes these variables. The values have to be set manually, depending on the expected behavior of the system.
All those variables are protected so your changes will be retained during the next generator run.
Additional variables can be declared here and will be retained.
See GivenHelpers.
Before:
In lines 32-36 the setup for the tests is prepared.
Dependencies are mocked:
The database is resetted.
Optional: Database is seeded with Events and/or Readmodels.
After:
The after function is releasing the Mocks to avoid side effects between tests.
Test Functions:
Here the actual test is implemented.
This is the test flow:
processedEvent
expectedEvent
in the event stream.The objective of the projection integration test is to test the happy path trough the application:
From projected event to projected entity.
An example of an integration test can be found under: Task/tests/integration/useCases/read/ProjectTaskOverview/ProjectTaskAdded.test.ts
1import { describe, it, beforeEach, after } from "mocha";2import { expect } from "chai";3import { MockHelper } from "tests/integration/utils/MockHelper";4import { IntegrationTestHelper } from "tests/integration/utils/IntegrationTestHelper";5import { TaskAddedEventMessage } from "shared/application/inboundEvents/TaskAddedEventMessage";6import { GivenTaskAddedEventMessage } from "tests/integration/utils/given/inboundEvents/GivenTaskAddedEventMessage";7import { defaultTaskOverview, TaskOverview } from "shared/application/readmodels/TaskOverview";8import { GivenTaskOverview } from "tests/integration/utils/given/readmodels/GivenTaskOverview";910const givenTaskOverview: TaskOverview[] = [11 GivenTaskOverview.readModel({}),12];13const projectedEvent: TaskAddedEventMessage = GivenTaskAddedEventMessage.inboundEvent({14 streamName: 'Taskbricks.Task',15 no: 1,16 aggregateId: '',17});18const expectedCreatedTaskOverview: TaskOverview[] = [19 {20 ...defaultTaskOverview,21 taskId: ,22 title: ,23 description: ,24 assigneeId: ,25 status: TaskStatusEnum.Open,26 },27];28const expectedUpdatedTaskOverview: TaskOverview[] = [29 {30 ...givenTaskOverview[0],31 taskId: ,32 title: ,33 description: ,34 assigneeId: ,35 status: TaskStatusEnum.Open,36 },37];3839describe('Projecting TaskAdded',40 /** @overwrite-protection-body false */41 async function() {42 beforeEach(async function() {43 MockHelper.mockSQSClient();44 await IntegrationTestHelper.resetDB();45 });4647 after(function() {48 MockHelper.restore();49 });5051 it('creates TaskOverview read model.', async function() {52 await IntegrationTestHelper.whenProjectingTaskOverview(projectedEvent);53 await IntegrationTestHelper.thenExpectTaskOverview(expectedCreatedTaskOverview);54 });5556 it('updates TaskOverview read model.', async function() {57 await IntegrationTestHelper.givenTaskOverviews(givenTaskOverview);58 await IntegrationTestHelper.whenProjectingTaskOverview(projectedEvent);59 await IntegrationTestHelper.thenExpectTaskOverview(expectedUpdatedTaskOverview);60 });61 });62
Key Concepts:
Given and Expectation Variables:
In the lines 10-37 the given state and expectations are defined.
The generator initializes these variables. The values have to be set manually, depending on the expected behavior of the system.
All those variables are protected so your changes will be retained during the next generator run.
Additional variables can also be declared here and will be retained.
See GivenHelpers.
Before:
In lines 43-44 the setup for the tests is prepared.
Dependencies are mocked:
The database is resetted.
Optional: Database is seeded with Readmodels.
After:
The after function is releasing the Mocks to avoid side effects between tests.
Test Functions:
Here the actual test is implemented.
This is the test flow:
This is the test flow:
projectedEvent
expectedTaskOverview
in the database.Depending on the projection type, there might be multiple tests performed.
The objective of the query api integration test is to test the happy path through the application:
From API Request to Response.
An example of an integration test can be found under: Task/tests/integration/useCases/read/AssigneeTaskOverview/AssigneeTaskOverviewApi.test.ts
1import { describe, it, before, after } from "mocha";2import { expect } from "chai";3import { APIGatewayProxyResult, parseToDateTime } from "@codebricks/typebricks";4import { MockHelper } from "tests/integration/utils/MockHelper";5import { IntegrationTestHelper } from "tests/integration/utils/IntegrationTestHelper";6import { AssigneeTaskOverviewApiRequest } from "useCases/read/AssigneeTaskOverview/infrastructure/AssigneeTaskOverviewApi";7import { handler } from "useCases/read/AssigneeTaskOverview/infrastructure/AssigneeTaskOverviewApiHandler";8import { defaultTaskOverview, TaskOverview } from "shared/application/readmodels/TaskOverview";9import { GivenTaskOverview } from "tests/integration/utils/given/readmodels/GivenTaskOverview";1011const givenTaskOverview: TaskOverview[] = [12 GivenTaskOverview.readModel({}),13];14const request: AssigneeTaskOverviewApiRequest = {15 assigneeId: '',16};17const expectedResponse: TaskOverview[] = [18 givenTaskOverview[0],19];2021describe('GET assignee-task-overview',22 /** @overwrite-protection-body false */23 async function() {24 before(async function() {25 await IntegrationTestHelper.resetDB();26 await IntegrationTestHelper.givenTaskOverviews(givenTaskOverview);27 });2829 after(function() {30 MockHelper.restore();31 });3233 it('returns response', async function() {34 const result: APIGatewayProxyResult = await handler(35 IntegrationTestHelper.toQueryApiGatewayEvent(request)36 );37 expect(result.statusCode).equal(200);38 expect(JSON.parse(result.body, parseToDateTime)?.data).to.deep.equal(expectedResponse);39 });40 });41
Key Concepts:
Given and Expectation Variables:
In the lines 10-22 the given state and expectations are defined.
The generator initializes these variables. The values have to be set manually, depending on the expected behavior of the system.
All those variables are protected so your changes will be retained during the next generator run.
Additional variables can also be declared here and will be retained.
See GivenHelpers.
Before:
In lines 27-32 the setup for the tests is prepared.
The database is resetted.
Database is seeded with queried Readmodels.
After:
The after function is releasing the Mocks to avoid side effects between tests.
Test Functions:
Here the actual test is implemented.
This is the test flow:
request
variable.expectedResponse
variableIn the integration tests Given Helpers are used to help with faking the seeded data.
These Helpers can be found under tests/integration/utils/given/readmodels/GivenTaskOverview.ts
1import { TaskOverview } from "shared/application/readmodels/TaskOverview";2import { OverwriteProtectionBody } from "@codebricks/typebricks";3import { faker } from "@faker-js/faker";4import { TaskStatusEnum } from "shared/domain/enums/TaskStatusEnum";56export interface GivenTaskOverviewProperties {7 taskId?: string;8 title?: string;9 description?: string;10 assigneeId?: string;11 status?: TaskStatusEnum;12}1314export class GivenTaskOverview {15 @OverwriteProtectionBody(false)16 static readModel(properties: GivenTaskOverviewProperties): TaskOverview {17 return {18 taskId: properties.taskId ?? faker.string.uuid(),19 title: properties.title ?? faker.string.alpha(),20 description: properties.description ?? faker.string.alpha(),21 assigneeId: properties.assigneeId ?? faker.string.uuid(),22 status: properties.status ?? faker.helpers.enumValue(TaskStatusEnum),23 };24 }25}
In here faker for the whole data structures are provided.
Additionally these functions can be overwritten for a more specific fakes.
Usage:
The idea of using these GivenHelpers is to only control as little amount of the data as necessary to avoid unexpected behavior by specific values.
This means you only have to specify the values you need to control in your test the rest will be faked.
1const userId: string = 'fc88309e-2b5d-4fd7-a5f4-7377e8c9bb40';2const givenTaskOverview: TaskOverview[] = [3 GivenTaskOverview.readModel({4 assigneeId: userId5 }),6];
In this example we only make sure the assigneeId
is initialized with the userId
the rest is faked.
Execute all unit tests running:
npm run test-unit
Execute a single unit tests running:
npm run test-unit-one %PATH_TO_TEST%
An example of an value object unit test can be found under: Task/tests/unit/valueObjects/TaskTitleValueObject.test.ts
1import { describe, it } from "mocha";2import { expect } from "chai";3import { ValidationError } from "@codebricks/typebricks";4import { TaskTitleValueObject } from "shared/domain/valueObjects/TaskTitleValueObject";56const validValues: string[] = ['', ''];7const invalidValues = [''];89describe('TaskTitleValueObject',10 /** @overwrite-protection-body false */11 async function() {12 it('test construct.', function() {13 validValues.forEach((validValue: string) => {14 const taskTitleValueObject: TaskTitleValueObject = new TaskTitleValueObject(validValue);15 expect(taskTitleValueObject.value).equal(validValue);16 });17 });18 it('test construct (negative).', function() {19 invalidValues.forEach((invalidValue: any) => {20 expect(() => new TaskTitleValueObject(invalidValue)).to.throw(ValidationError);21 });22 });23 it('test equals.', function() {24 const taskTitleValueObject0: TaskTitleValueObject = new TaskTitleValueObject(validValues[0]);25 const taskTitleValueObject1: TaskTitleValueObject = new TaskTitleValueObject(validValues[0]);26 expect(taskTitleValueObject0.equals(taskTitleValueObject1)).to.be.true;27 });28 it('test equals (negative).', function() {29 const taskTitleValueObject0: TaskTitleValueObject = new TaskTitleValueObject(validValues[0]);30 const taskTitleValueObject1: TaskTitleValueObject = new TaskTitleValueObject(validValues[1]);31 expect(taskTitleValueObject0.equals(taskTitleValueObject1)).to.be.false;32 });33 });34
Key Concepts:
Given Variables:
In line 6-7 the values used in the Unit tests are defined.
Provide valid and invalid values for the Value Object here.
Test Functions:
The following tests are performed: