When writing MUnit tests we can borrow a lot from what has been learned from writing effective unit tests using other tools, such as JUnit. The subject under test being a flow instead of a java method, is somewhat different so some interpretation is necessary. Depending on the applications, and the testing strategies used, MUnit tests can be complicated or simple.
In Part 1 of this two-part blog, we will adapt those existing best practices to MUnit tests. In Part 2, I will show you how we can use the tools and libraries from MuleSoft libraries as best we can to create these tests. Once we have these practices and tools in place, we can fit our tests into more advanced strategies for integration testing, continuous integration and test-driven development.
MUnit test process
The MUnit process can be broken down into four major steps. These include the following:
Creating the test cases
Reviewing the test cases
Executing the tests on individual segments of the code.
All of these steps together can help build automated tests for software development for business automation techniques. One important idea here is to maintain proper naming conventions while running the tests. This is also applicable for devices on the cloud and on premises. Maintaining naming procedures can help make the process simpler and also helps distinguish each piece of code from the rest.
There are 5 key parts that make up a good MUnit test. These parts taken as a whole will ensure our tests meet the expectation of developers in our community and help provide the quality expected by the stakeholders. Unit testing, however, cannot replace functional and system testing. It is just the first and a very important component in testing.
1. Test One Flow at a Time
A unit test should test one flow at a time. If the test fails, we should know immediately what failed, based on the flow under test. Mocking out back-end system calls help eliminate variations of back-end data or interface changes so testing is confined to code in the flow. We can also mock out sub-flow calls, but this is not something that has to be done as a general rule.
If a flow calls a sub-flow that is really part of the main flow, then it may make sense to make that part of the test case. If however the sub-flow is reusable, then perhaps it should be tested separately. For example, if the main flow has some data enrichment step or normalized the data for the sub-flow, then testing the sub-flow separately can be difficult. In these scenarios, allowing the main flow to run into the sub-flow makes more sense. Testing them together ensures the interface between the two works and continues to work as expected as well.
2. Mocking Out External Resources
When to mock out external calls to databases, SOAP or REST service calls, and queues depend on a few factors. If for example you have a Docker image that can simulate the external system with a controlled set of data, then perhaps the database or external system may not require mocks. If you don’t have this and are tempted, for example, to just use the DEV environment--something not well controlled--then instead you should mock out this external call.
In the later case (using the DEV environment in a test) you will not be able to reliably create scenarios for the MUnit test. In the former case (using a Docker container in the test) the tests can be controlled and you get the added benefit of testing for interface or schema changes that can break your code and require refactoring. With tests reliant on a shared DEV environment for instance, the tests are more likely to fail due to data issues and over time the concern we have for failed tests start to wane. At this point all tests begin to lose their effectiveness. A failure that may have caught a bug, for instance, may get ignored with these data-related issues. At this point our investment in MUnit is lost. So for these cases we should not be testing with an actual back-end system and we should mock the external service call. Testing for interface changes will have to be done with functional and system-level tests.
In Part 2, you will see how the MUnit module will allow you to mock out these calls and create spies to test request data going to them. You will see how to make assertions about expected results coming from the process flow. With these services mocked out, we can create specific scenarios needed to test aspects or our mule flows.
3. Create Independent tests
Each test you create should test one independent aspect of the flow. An aspect may be a particular branch or condition in the flow, data-mapping scenario, or null, missing and invalid data tests. Obviously, that is not always possible. Some paths or aspects of the flow are common and will be tested repeatedly, regardless of how you design the test. The objective, however, is to create tests that will pass or fail for a particular scenario, and that do not fail, redundantly, when other tests fail.
In creating multiple tests for a particular flow, it is convenient to copy/paste an existing test for new tests and modify the input or mock service data. This is fine to do, but be sure to remove duplicate assertions from the flow and create test cases that will ensure all paths in the flow are tested. This includes testing conditional paths in choice routers, Dataweave scripts, and exception flow paths.
Your project will probably live in production for a long time. Over time, however, developers leave the project, new ones come on, things are forgotten and changes are made. Writing good, independent tests like this is like writing little reminders to ourselves and others. These tests should ensure that all the nuances we put in the code don’t get overlooked by someone in the future.
4. One Test One Focus Aspect
One test one assertion was the rule for Java, but doesn’t make as much sense for MUnit tests. When writing independent MUnit tests, it is good practice to focus on one aspect for each test, however, like the normal flow versus conditional paths or exception paths.
For example, if a flow has a normal path and an exception path, two test cases may be sufficient. You may need more tests if you have particular nuances in the data that need to be tested. These nuances could involve empty array lists, or null or missing arguments. If your flow contains complex Dataweave scripts, with if-then-else statements, filters, aggregators or sorting you will want to create more tests. However these tests should not contain the same assertions found in the base-line tests.
5. Designing Flows for Testability
In order to achieve the objectives above, you will want to design your code for testability. For example, take a look at the following code.
We know already we will need at least three test cases, one for each choice-router path. To do this we will be running through the same path up to the choice router. It may be better to re-factor this flow with the choice router in a sub-flow. This way we can isolate and test the choice router separately from the common aspects of the main flow.
Likewise we may need many tests for a Dataweave script to test it properly. In that case we will want to move it into a sub-flow and test it in isolation. This will help create independent tests and one test, one assertion. Be sure to mock out references to these sub-flow when testing the main flow.
Importance of Testing/MUnit testing
Agile process – Making any change to an existing piece of software means you are altering the code. And re-testing and deploying this product might be a very expensive process. As a result, you can use MUnit tests to make it a more agile procedure.
Identifies software bugs – Unit testing provides the superior advantage of identifying bugs in the software at a very early stage, thereby giving the developer sufficient time to rectify the issues. In effect, when you figure out the issue early, as you progress, no other part of the software gets impacted.
Change management – MUnit testing helps facilitate the process of change management. Whenever any code library is updated, when you run MUnit tests, you can actually make changes easily since each of its units is tested and validated before release. It also paves the way for smoother integration with other parts of the tool.
Software designing – The design of the software is an important component of the software development lifecycle. The developer usually has to sketch out the design in advance before the coding starts. However, with MUnit testing, the developer can design each part of the code and test it to make it more robust. In turn, if there are any design changes to be made, this would be a good time to implement them.
Cost effective – MUnit testing makes the whole software development process cost effective. How does that work? This test basically tests each segment of the software as and when it is developed. As a result, if there are glitches or bugs, you can identify them during the development process, and you do not have to wait till the entire development is complete. The latter is a more expensive method.
Effective documentation – Documentation is key to any software development project. What this means is that what part of the code does what in the process needs to be documented in order for the new developers or users to understand the method. This makes it a more fool-proof process.
MUnit Testing Tips
Maintain appropriate naming convention for each test you undertake.
Write smart tests to put a major part of the software component at test, by utilizing minimal coding.
Maintain a clean and easy interface for each MUnit test.
Sometimes, mock tests come in handy in order to conduct MUnit tests on live databases and environments, like devices in the cloud.
It is imperative to write the MUnit test codes before fixing the bug.
Make MUnit tests for concurrent codes which are usually the source of multiple bugs.
Conduct multiple tests in order to bring out the most robust solution.
It is best to use Anypoint platform desktop IDE to streamline the steps required for building your connector.
Using the objectives provided here, you can create MUnit tests using proven techniques developed by java developers but adopted for MuleSoft flows. In Part 2, we will put these objectives into practice using tools provided by MuleSoft in the MUnit and Dataweave modules.