You are viewing documentation for a beta release that is currently undergoing final testing before its official release. For the latest stable version click here.
Testing
Writing (unit) tests is a crucial part of software development as it ensures that all the components of an application work as expected. This page covers how to test a typical Moleculer-based application.
Testing Frameworks
Please note that we use Jest for testing. However, you can also use any other testing framework that offers the same capabilities.
Common File Structure
The snippet presented bellow is a skeleton structure for writing unit tests for a Moleculer service.
describe("Test '<SERVICE-NAME>'", () => { // Create a service broker let broker = new ServiceBroker({ logger: false }); // Create the actual service let service = broker.createService(ServiceSchema);
// Start the broker. It will also init the service beforeAll(() => broker.start()); // Gracefully stop the broker after all tests afterAll(() => broker.stop());
/** Tests go here **/ });
To test the service two things are required: the ServiceBroker class and the schema of the service that is going to be tested. Next thing to do is to create an instance of ServiceBroker and, after that, create the actual instance of the service. Then Jest’s beforeAll() helper function is used to start the service broker and, after all tests are complete the broker is stopped with the afterAll().
With this setup in place we are ready to write the actual tests.
TIP: Disable the logs, by setting logger to false during broker creation, to avoid polluting the console.
Unit Tests
Actions
Simple
A typical (yet very simplistic) action looks like the one presented bellow:
The toUpperCase action of helper service receives a parameter name as input and, as a result, returns the uppercase name. This action also emits an (name.uppercase) event every time it’s called. Moreover, the toUpperCase has some parameter validation, it only accepts name parameter if it’s a string. So for the toUpperCase action there are three things that could be tested: the output value that it produces, if it emits an event and the parameter validation.
// Check the result expect(result).toBe("JOHN"); });
it("should reject with a ValidationError", async () => { expect.assertions(1); try { await broker.call("helper.toUpperCase", { name: 123 }); } catch (err) { // Catch the error and see if it's a Validation Error expect(err).toBeInstanceOf(ValidationError); } });
it("should emit 'name.uppercase' event ", async () => { // Spy on context emit function jest.spyOn(Context.prototype, "emit");
// Call the action await broker.call("helper.toUpperCase", { name: "john" });
// Check if the "emit" was called expect(Context.prototype.emit).toBeCalledTimes(1); expect(Context.prototype.emit).toHaveBeenCalledWith( "name.uppercase", "john" ); }); }); });
DB Adapters
Some actions persist the data that they receive. To test such actions it is necessary to mock the DB adapter. The example below shows how to do it:
const DbService = require("moleculer-db");
module.exports = { name: "users", // Load the DB Adapter // It will add "adapter" property to the "users" service mixins: [DbService],
actions: { create: { handler(ctx) { // Use the "adapter" to store the data returnthis.adapter.insert(ctx.params); } } } };
describe("Test 'users.create' action", () => { it("should create new user", async () => { // Replace adapter's insert with a mock usersService.adapter.insert = mockInsert;
// Call the action let result = await broker.call("users.create", { name: "John" });
// Check the result expect(result).toEqual({ id: 123, name: "John" }); // Check if mock was called expect(mockInsert).toBeCalledTimes(1); expect(mockInsert).toBeCalledWith({ name: "John" }); }); }); });
Events
Events are tricky to test as they are fire-and-forget, i.e., they don’t return any values. However, it is possible to test the “internal” behavior of an event. For this kind of tests the Service class implements a helper function called emitLocalEventHandler that allows to call the event handler directly.
module.exports = { name: "helper",
events: { async"helper.sum"(ctx) { // Calls the sum method returnthis.sum(ctx.params.a, ctx.params.b); } },
methods: { sum(a, b) { return a + b; } } };
Unit tests for the helper service events
describe("Test 'helper' events", () => { let broker = new ServiceBroker({ logger: false }); let service = broker.createService(HelperSchema); beforeAll(() => broker.start()); afterAll(() => broker.stop());
// Call the "helper.sum" handler await service.emitLocalEventHandler("helper.sum", { a: 5, b: 5 }); // Check if "sum" method was called expect(service.sum).toBeCalledTimes(1); expect(service.sum).toBeCalledWith(5, 5);
// Restore the "sum" method service.sum.mockRestore(); }); }); });
Methods
Methods are private functions that are only available within the service scope. This means that it’s not possible to call them from other services or use the broker to do it. So to test a certain method we need to call it directly from the service instance that implements it.
module.exports = { name: "helper",
methods: { sum(a, b) { return a + b; } } };
Unit tests for the helper service methods
describe("Test 'helper' methods", () => { let broker = new ServiceBroker({ logger: false }); let service = broker.createService(HelperSchema); beforeAll(() => broker.start()); afterAll(() => broker.stop());
describe("Test 'sum' method", () => { it("should add two numbers", () => { // Make a direct call of "sum" method const result = service.sum(1, 2);
expect(result).toBe(3); }); }); });
Local Variables
Just as methods, local variables are also only available within the service scope. This means that to test them we need to use the same strategy that is used in methods tests.
module.exports = { name: "helper",
/** actions, events, methods **/
created() { this.someValue = 123; } };
Unit tests for the helper service local variables
describe("Test 'helper' local variables", () => { let broker = new ServiceBroker({ logger: false }); let service = broker.createService(HelperSchema); beforeAll(() => broker.start()); afterAll(() => broker.stop());
Integration tests involve testing two (or more) services to ensure that the interactions between them work properly.
Services
Situations when one service depends on another one are very common. The example bellow shows that notify action of users service depends on the mail service. This means that to test the notify action we need to mock the send action of email service.
describe("Test 'users' service", () => { let broker = new ServiceBroker({ logger: false }); let usersService = broker.createService(UsersSchema);
// Create a mock of "send" action const mockSend = jest.fn(() =>Promise.resolve("Fake Mail Sent")); // Replace "send" action with a mock in "mail" schema MailSchema.actions.send = mockSend; // Start the "mail" service let mailService = broker.createService(MailSchema);
describe("Test 'users.notify' action", () => { it("should notify the user", async () => { let result = await broker.call("users.notify");
expect(result).toBe("Fake Mail Sent"); // Check if mock was called expect(mockSend).toBeCalledTimes(1); }); }); });
API Gateway
The logic that our services implement is also usually available via API gateway. This means that we also need to write integration tests for the API gateway. The example bellows show to to it:
Testing Frameworks
Please note that for the API gateway tests we use supertest. Again, this is not mandatory and you can use any other tool that offers the same capabilities.