The Usual Suspects
Usually, some testing frameworks are chosen within a company or team, so switching teams is more relaxing. This by itself is a good thing. However, that is not the issue that this post addresses. It is more about the fixation on the chosen testing framework: no longer seeing tests, but rigid, isolated structures.
xUnit, NUnit and Microsoft’s Test framework are probably the most popular C#-minded testing frameworks. At Codit, xUnit is often used (not to be mistaken with the xUnit testing patterns). Writing tests with xUnit is either with the [Fact]
or [Theory]
attribute.
Theories allow for data-driven tests. The data can be placed on the test itself in [InlineData]
attributes, in a dedicated static
member in the test class where the tests live. This gets assigned with an [MemberData]
attribute, or in a separate class, so it can be used in multiple test classes and is assigned with the [ClassData]
attribute. For more information on xUnit, see their project site.
The problem here is maintenance and reusability. Once you’re in the world of xUnit (or any other C#-minded framework), you start thinking in facts and theories and about where the data should be put in the three supported places, instead of thinking about tests. This is not an obvious problem but something that will pop up when you’re writing a lot of tests. You have one in your head and end up spending your time figuring out if your framework supports it, instead of writing the test.
Tests as First-Class Citizens
So, how do we circumvent this problem? The F# Expecto testing library has a solution for this that is so elegant and simple that it borders on the brilliant. Expecto sees tests as first-class citizens. A test, like any other component in an F# environment, is just a function. A list of tests, a fixture-driven test, data-driven, property-based test? It’s all a simple function.
Expecto tests can be easily written with a test builder that creates a function, or a test function itself.
There’s not much to say about this first basic test example. Both approaches have a different way of describing tests, but there are no major differences.
However, imagine that you want to re-use the color data in the previous example in more than a single test. The changes that will need to be made are fundamentally different in the C#-variant and F# Expecto environment.
This is the xUnit update:
This is the update to Expecto:
While in xUnit and other test frameworks, you will need to focus on where the test data should be put. For all the related tests to have access to it, you will mostly focus on tests in Expecto. The test data in Expecto is already in the right kind of place and format, but now a new test needs to be added. This is not only a major improvement in time efficiency, but it also helps the test writer to fully submerge themselves in writing tests.
Boundless Functional Testing vs. Object-Oriented Compartmentalization
We’ve already seen a big difference in style between thinking in object compartments and flexible functions, but there’s more. One thing that almost all tests do is change. Tests are often seen as a set of statically defined pieces of code that only cause trouble during test runs. One of the reasons for this is that the testing infrastructure is not seen as a “workplace”, as the released software is not located there. Another reason for this is that changing things around takes a lot of time and refactoring. Slowly, the test suite is becoming the dirty laundry of the application.
That’s why I’m such a fan of Expecto. Instead of reinventing new kinds of concepts like facts and theories, they re-use the core principle of functional programming and make their tests act like regular functions. This means that flexibility, language support and easy understanding are built-in. Without the hurdles of thinking about the boundaries of your testing framework, you can focus on the thing that matters most: tests.
The last example: one of the things that I come across quite a lot, is that you start from a set of tests and then want to pass in an additional argument. This could be a target framework or all the available options of a setting. This is a very common situation, where you start from a place that needs changing and you would like your tests to reflect this. In this example, I would love to pass in a pencil size of the colors we’re testing.
Notice how I didn’t change the actual content of the tests or the previously defined colors test data. I only passed in a size
value that the tests can use. Then, I used the existing set of tests as a function for a new definition of tests that will pass in the pencil size. The strength of this way of working cannot be overestimated. Imagine this on a large scale, with hundreds or thousands of tests. In a xUnit environment, you will need to change the test data of all those tests. It doesn’t even stop there: imagine if some of those tests become test properties, or need to dispose of a test fixture, or more than one set of data? You could work with class data and try to re-use as much as possible to limit your time and other tricks but it will never be as powerful and descriptive as you want it to be.
If you’re still not convinced of the extensibility and flexibility of Expecto, the testParams
and testParamsMany
functions used here aren’t even part of the default F# Expecto package. Those are functions I’ve written to make sure that the tests look as much to the C#-minded variants as possible.
This level of extension cannot be achieved with testing frameworks that try to place something as fluxive as tests in rigid compartments. Tests require change and this high standard of reusability if we want the test environment to be as fun to work in as the production code.
Conclusion
Writing tests in F# Expecto is a wonderful way of working. To be able to focus solely on tests is a very freeing experience. Not worrying about what kind of syntax your test framework use, or if it’s supported is great. Because Expecto is based on functions, you can really define tests the way you visualize them in your head. In this post, I moved some test infrastructure to dedicated functions so that the actual content of the test was spared. In more complex situations, you could have test functions that reflect your domain. In this case, this could be something like testColors
for example, which would further hide unnecessary information from the test content. Unlike other libraries, you are freer to do this, as you are only bound by the limitations of your programming language and your imagination.
Thank you! I hope you had as much fun reading this as I did writing it.
Stijn
Subscribe to our RSS feed