Magic is often used to describe confusing code. Often we cannot explain how something works so magic seems like an apt description.
The problem with magic is that it’s an entirely subjective term which can be used as a very annoying stick to hit useful abstractions.
A while ago I wrote something along the lines of this in Scala.
val transformedCollection = collection map transformer
For those not familiar with the
map construct, this will run the
transformer function on each item in the
collection, returning a new collection of transformed items.
Another nameless programmer chastised me
"I like to be pragmatic"
(Who exactly doesn’t like to be pragmatic?)
"This is magic, why not just write a for-loop, it’s clearer"
Apart from the pragmatic comment it’s actually a reasonable thing to say, if you don’t understand how something works then it’s magical!
Understand the magic
Just because something seems magical doesn’t mean it’s wrong. The problem with the term is it’s often lazily used when someone doesn’t understand an abstraction, or worse - doesn’t want to understand.
So what is magic?
For me I get the feeling of dark arts surrounding me when there is a lack of explicitness. Generally stuff changing “underneath” me based on some kind of state of the system. It often feels like the code is working by accident rather than deliberately.
Anti-patterns like global imports and setter-based DI makes the system feel magical in a very ugly way.
Choose your spells
From time to time all of us look at some code and throw our hands up in the air when we don’t initially understand something. Rather than unsheathing our anti-magic sticks make an attempt to understand it first.
There’s nothing more annoying than someone describing your code as magic when you feel you put some effort into a useful abstraction. So try to be considerate when faced with unfamiliar constructs.
That being said it is important for programmers to share their knowledge when introducing something new. Be sure to explain your sleight of hand at code review and use abstractions tastefully and keep intent clear at all times.
Don't lazily conflate the unfamiliar with magic. It's short-sighted and annoying.
This posts hopes to illustrate
- What property based testing is and why it is an important tool to compliment your existing tests
- How easy it is in Go
Your current tests
The usual examples illustrating TDD could be described as example based tests; where you write tests by providing example inputs and expected outputs.
When you realise that they are just examples, you see your code perhaps isn’t as well-tested as you think.
Wouldn't it be nice if it were easy to provide thousands of different test inputs for your tests?
You may have already heard about property-based testing and think its about testing your code with randomly generated data to exercise different scenarios; but it actually requires a shift in mindset to get the most out of it.
Specifications, not just lots of data
Property-based testing is called that because in order to write the tests you have to think about the properties of your domain in order to write effective tests.
An example with addition
We will test an add function, which just adds 2 integers together.
The tricky thing when you feed in random data, is how do you know what your expected result should be?
You may be tempted to do the above, but in effect you’re cheating because you’re using the implementation to verify itself.
Pretend that you’re testing a more complicated function. You know you shouldn’t do the following:
This isn’t really testing anything, if you change the implementation of
complicated it will continue to pass its tests even if its behaviour is incorrect.
(This is a common problem with bad tests where the writer has re-implemented the unit they are testing, in the test!)
But what do you do? How can you verify your function on random inputs?
The properties of addition
What is there to addition anyway? Well, there’s a few things:
when you add zero to a number, you get the same number back again
the order of the inputs does not matter
Let’s write some property based tests based on these laws of addition.
quick.Check function will run your assertion 100 times (by default), feeding in randomly generated data for the inputs required. If it fails at any point then you will be told for what inputs your function fails on so you can go reproduce it and fix.
- By asserting on properties rather than known values we don’t have to "cheat" on the tests like I did earlier.
- We have now described what addition is in terms of its properties, which is more expressive than the example based test from earlier.
- We now have a more robust test suite, able to run thousands of different inputs in milliseconds.
Thinking abstractly about your domain
Once you take a step back and really think about the domain of your code, you may discover some unexpected properties that the examples you use in your general TDD flow wouldn’t find.
I imagine this is how a lot of QAs think. They tend to be a bit detached from the code we write and really think about the domain and how things can fall apart. Property based testing allows us to capture these kind of rules as tests.
Going beyond simple types
Property-based testing relies on generators for types to create random data for your tests. It’s simple to imagine how this works for integers but what about your own types?
All you have to do is implement the Generator interface for your type. Go will provide you with a
rand to help you generate random data.
To write effective property-based tests you have to take a step back and think deeply about the rules of your domain.
Once you gain this deep understanding you can then express them very easily with property-based tests which will help you find bugs before you get to production.
The tooling in Go facilitates easily writing these specifications, even for your own types so there’s no excuse not to try it in your current project.