Testing

The Codebricks Generator generates also Test Code. This comprises integration tests and the corresponding helpers and unit tests.

Integration 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%

Command API 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";
9
10const 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];
23
24describe('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 });
33
34 after(function() {
35 MockHelper.restore();
36 });
37
38 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:

  • Clock (Timestamps)
  • UuidGenerator (Uuids)
  • SQS (No Communication)

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:

  • Invoke the APIHandler with an APIGateway event using the request variable.
  • Expect the status code to be 200
  • Expect the body of the response to be equals to the expectedResponse variable
  • Expect the expectedEvent in the event stream.

Command Policy Test

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";
10
11const 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];
27
28describe('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 });
37
38 after(function() {
39 MockHelper.restore();
40 });
41
42 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:

  • Clock (Timestamps)
  • UuidGenerator (Uuids)
  • SQS (No Communication)

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:

  • Process the processedEvent
  • Expect the expectedEvent in the event stream.

Projection Test

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";
9
10const 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];
38
39describe('Projecting TaskAdded',
40 /** @overwrite-protection-body false */
41 async function() {
42 beforeEach(async function() {
43 MockHelper.mockSQSClient();
44 await IntegrationTestHelper.resetDB();
45 });
46
47 after(function() {
48 MockHelper.restore();
49 });
50
51 it('creates TaskOverview read model.', async function() {
52 await IntegrationTestHelper.whenProjectingTaskOverview(projectedEvent);
53 await IntegrationTestHelper.thenExpectTaskOverview(expectedCreatedTaskOverview);
54 });
55
56 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:

  • SQS (No Communication)

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:

  • Project the projectedEvent
  • Expect the projected entity expectedTaskOverview in the database.

Depending on the projection type, there might be multiple tests performed.

Query API Test

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";
10
11const givenTaskOverview: TaskOverview[] = [
12 GivenTaskOverview.readModel({}),
13];
14const request: AssigneeTaskOverviewApiRequest = {
15 assigneeId: '',
16};
17const expectedResponse: TaskOverview[] = [
18 givenTaskOverview[0],
19];
20
21describe('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 });
28
29 after(function() {
30 MockHelper.restore();
31 });
32
33 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:

  • Trigger the APIHandler with an APIGateway event using the request variable.
  • Expect the status code to be 200
  • Expect the body of the response to be equals to the expectedResponse variable

Given Helpers

In 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";
5
6export interface GivenTaskOverviewProperties {
7 taskId?: string;
8 title?: string;
9 description?: string;
10 assigneeId?: string;
11 status?: TaskStatusEnum;
12}
13
14export 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: userId
5 }),
6];

In this example we only make sure the assigneeId is initialized with the userId the rest is faked.

Unit Tests

Execute all unit tests running:

npm run test-unit

Execute a single unit tests running:

npm run test-unit-one %PATH_TO_TEST%

Value Object Unit 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";
5
6const validValues: string[] = ['', ''];
7const invalidValues = [''];
8
9describe('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:

  • Construct with valid values.
  • Construct with invalid values.
  • Equals returns true.
  • Equals returns false.

© 2024 Codebricks | All rights reserved.