Clean code in low-code: test-driven development in Mendix
Bringing the high-code practice to low-code for robust and testable low-code software.
This article is the first part of (what will hopefully become) a series on quality assurance and test-driven development in Mendix.
What is test-driven development?
Test-driven development is a software development practice where developers create (automated) tests before writing the actual code that is tested. It is an iterative process, also known as red-green-refactor, of writing tests that fail (red), creating the actual code to make those tests pass (green) and refactoring that code.
Like with Agile, Scrum and basically every other methodology used in the IT industry, there are many different interpretations of test-driven development, each with their own advantages, disadvantages and unique quirks.
In many ways I am somewhat of a purist, so I tend to use the definition given by Kent Beck, who is widely regarded as the one that developed this technique:
Test-driven development is a programming workflow. […] TDD is intended to help the programmer create a new state of the system where 1) everything that used to work still works, 2) the new behavior works as expected, 3) the system is ready for the next change and 4) the programmer & their colleagues feel confident in the above points.
In its most basic form, TDD consists of five steps:
Now, to be fair, the vast majority of these types of workflows were developed and are used in the high-code world of C, C++, Java, etc. They’re called ‘programming workflows’ for a reason.
But broadening our look a bit, expanding their scope to ‘software development’ in general instead of just (high-code) ‘programming’, can’t this workflow be used in low-code, too? After all, we as low-coders develop software as well, just not in the traditional writing-code-line-by-line way, but by modeling: visualizing an app’s data in domain models and its logic in flows.
So how would you go about using TDD in a low-code environment like Mendix?
Unit testing in Mendix
As TDD is all about unit testing, it all starts with the humble UnitTesting module from the Mendix Marketplace…
Just like a unit test in a high-code programming language is a piece of code that tests another piece of code, a unit test in Mendix is a microflow that runs another microflow and checks the results.
And, also like unit tests in high-code, a unit test in Mendix should be isolated, small and fast.
The UnitTesting module comes with everything you need to create and run unit tests, like microflows to assert the result of a unit test, a UnitTestOverview snippet that can be put on a page to load, reload, run and display the results of all unit tests in your app, and even a built-in API for running the unit tests remotely. Every microflow that has a name starting with either ‘UT_’ or ‘Test_’ (not case sensitive) is recognized by the module as a unit test and so will turn up in the test overview when reloaded.
What is a ‘unit’ in Mendix?
At ABN AMRO, we consider all microflows, rules and Java actions (let’s call them functions) to be units for unit testing purposes, unless there is interaction with something at or beyond the ‘edge’ of the system.
Interacting with something beyond the edge of the system means one or more of the following:
Unit tests test the system logic within the system’s boundaries and without changing the state of the system. As soon as a function includes (and passes) such a boundary, you get into the domain of service/integration and system/UI tests.
OK, now we know what TDD is and that we need the UnitTesting module from the Mendix Marketplace, let’s see what TDD actually looks like in Mendix!
Test-driven development in Mendix
Let’s go at this step by test-driven-development-step, assuming we already have a list of tests.
We are going to test a piece of logic of FizzBuzz, a well-known TDD exercise that is about turning numbers into the strings ‘Fizz’, ‘Buzz’ or ‘FizzBuzz’ if they are a multiple of 3, a multiple of 5 or both, respectively.
In this case, we are testing turning an integer into a string.
Recommended by LinkedIn
Write a (failing) test
We start by creating a unit test, and as we want to turn an integer into a string, adding a ‘Create Integer variable’ activity to it and give that variable a value of 1.
When we then add the ‘Call microflow’ activity so we can run the flow under test, we hit our first error: the fact that the microflow that we want to test doesn’t exist yet.
This may seem trivial, and it is, but it serves a clear purpose: we now have to create a microflow that accepts an Integer as input and should return a String as output, the first step in incrementally creating the full FizzBuzz feature.
Write the code to make the test pass
We now create the microflow we want to test, let’s call it FizzBuzz_Main, make sure it has an input parameter of type Integer, and returns a String.
Now remember, we only have to make the test pass, and the test passes the number 1 to the microflow, so the only thing we have to do to make the test pass is return the Integer 1 as the String ‘1’.
We’ll add an assertion to the test, making sure the flow under test returns the stringified ‘1’ (which it does because we hard-coded it like that), run it, and see that it passes now.
This seems odd, to say the least, right? I just hard-coded a return value, just to make a test pass. Wouldn’t it be better to just do it right the first time? Well, yeah, it would, but do we already know what the right way to do this is? Sure, in this very simple case we might, but if we would have been working on a much more complex piece of logic, we might not.
Just as in the previous step, the point here is to incrementally build up the logic, so we can also incrementally test it and, by rerunning the tests at every step, make sure it keeps working. We basically fake it until we make it: the output of (this version of) this flow is obviously a fake, but with each step we take and each test we write after this one, we refactor the logic and fake it a little less, until we have the full feature, refactored and working as it should, fully covered with tests that all pass.
And speaking of refactoring…
Refactor
Now that the test passes, it’s time to refactor. On second review, I don’t like the fact that the microflow returns a hard-coded ‘1’, so what we could do is make sure it stringifies all integers, not just return a hard-coded value:
If we run the test again, we will see that it still passes, and we can go on to the next test.
This small refactor has a major benefit for all subsequent tests: it makes the microflow more generic, so it works for all integers, not just 1.
This is something you will see again and again in test-driven development and is one of the driving principles of the technique: as the tests get more specific, the code gets more generic. Obvious correct and incorrect cases, as well as edge and corner cases, all are covered by the tests and, once they pass, are handled correctly by the actual code.
Conclusion
Is TDD possible in Mendix? Yes, and it is pretty awesome.
Test-driven development is about thinking about what you need to test to make sure your software does what it should — and ensuring that it keeps doing that without breaking after you change it. It makes for robust, testable and test-covered software.
TDD also encourages an iterative development process, step-by-step building up the program’s logic by breaking it up into discreet, easy to understand, quick to develop and — more importantly — easily testable chunks. Testing and re-testing our software with every step makes it as robust and resistant to breaking as we can make it, because we only continue development when we have made sure all tests pass at any given moment. So we know that if something that worked earlier breaks, it can only be because you broke it with the last changes we made and we know where and how to fix it.
In addition, seeing those angry red, failing tests turn green as you solve problem after problem is very satisfying indeed…
Should everyone do TDD? No. At least, not just like that, without proper preparation.
Test-driven development requires a different mindset than the one we are often used to. Instead of focusing on delivery speed, building-as-you go and testing later (if ever…), TDD requires you to take a step back and think about what the program should actually do.
Of course, thinking about what a program should do is always the first step in every software development process, but TDD adds another dimension to that: it requires you to take a step back and postpone diving headlong into the nitty-gritty details of the implementation and think about the thing it should do and how to test that thing.
That being said, is test-driven development possible in Mendix? Yes, and it is awesome.
It's great to see you advocating for test-driven development in Mendix, Bart Zantingh! Your expertise in this area will surely help many developers enhance their app-building skills. Looking forward to your article and the upcoming meetup!
Interesting take, Bart Zantingh. How often have you been able to convince fellow Mendix developers in your teams to apply TDD?
Well written article Bart! Easy to read. Wouldn't it be fantastic if you could: - save document - auto deploys, runs tests in background - see up-to-date unittest report right in Studio pro Stay focussed without leaving Studio pro. Looking forward to the meetup