https://www.scalawilliam.com/unit-testing-is-simple/
Is unit testing difficult?
No, it is not. You don't even need a testing framework for it.
You don't even need to write the test in a separate file or class.
You just need to exit with code 1. This is enough to fail a Jenkins build and a Makefile!
Why is a unit test useful?
- Prevent regressions when you change the code.
- Document expected input and output as code.
- Replace error-prone println-driven development with test-driven development.
What do you test? when the logic is "complex enough" I know it could be broken 6 months down the line, due to "that one small change" - or that I'd spend time building it with print statements. Example from my own code.
Here, I'll show you the most basic way to unit testing with four languages: Bash, Python, Node.js, Scala, just to prove the point.
These examples are for a very basic piece of code and are intentionally minimalistic, to demonstrate that you needn't use a test framework to get started with - though one will be very necessary when you scale your application.
Bash
Here's a bash script inc.sh
with a basic assertion and some main code:
#!/bin/bash
increment() {
echo $(("$1"+1))
}
# If this assertion fails, the whole thing quits
[ 3 -eq $(increment 2) ] || exit 1
# Increment a stream of numbers
while read n; do
increment $n;
done
If we run:
$ seq 1 5 | bash inc.sh
2
3
4
5
6
$ echo $?
0
Exit code is 0. But if we broke the function, and put 2 instead of 1, we get:
$ seq 1 5 | bash inc.sh
$ echo $?
1
Python
This is easy to achieve in any language really. Python assertions:
Broken test in file inc.py
:
def increment(n):
return n + 2
assert increment(2) == 3
$ python inc.py
Traceback (most recent call last):
File "inc.py", line 4, in <module>
assert increment(2) == 3
AssertionError
$ echo $?
1
Node.js
Comes with assertions built in.
Broken test in file inc.js
:
const assert = require('assert');
function increment(n) {
return n + 2;
}
assert.equal(2, increment(1));
$ node inc.js
assert.js:81
throw new assert.AssertionError({
^
AssertionError: 2 == 3
...
$ echo $?
1
Scala
Comes with assert.
def inc(n: Int) = n + 2
assert(inc(1) == 2)
$ scala inc.scala
java.lang.AssertionError: assertion failed
$ echo $?
1
Why is this important?
You can get started light with several assertions and work your way up to proper test suites.
So what are test frameworks for?
To make testing more organised. You get:
- Organisation
- Detailed error messages
- Detailed test reports
- Neat assertions and human readable language (see ScalaTest)
- Incremental/TDD type of development
Conclusion
Now you have no excuse for not writing at least a few tests in your code.
Top comments (22)
Sadly, most posts/introductions about unit tests are usually way too simple with easy pure functions that just calculate some number. You don't have this kind of logic in real-world applications.
I have to deal with way more complicated scenarios daily involving object graphs that require initialization, external dependencies, drag and drop, framework objects that you can't mock and so on. I've yet to read a tutorial (or even book) that covers these cases.
How do I deal with legacy code that is not necessarily bad, but doesn't use DI and is not as easily testable as it ideally should be? How do I test public methods that don't return a value, but change something deeper down the call stack (internally)? I'd really like to read an article about how to tackle these problems!
Great questions! I actually have a whole lot to say on this but that'd have to be a whole new article.
For greenfield, functional programming is the answer. You don't need to test "changes deeper down the call stack" because there's no such thing as a change. Pass a value, get a value back.
We've written large complex applications with two flavours of code:
Both flavours test-driven, not test-after.
You say that this legacy code is not necessarily bad but missing testability. Assuming this, I say the following:
The cost of fixing is still less than rewriting from scratch. See Joel Spolsky's article about "Things you should never do".
Pick up a copy of Working Effectively with Legacy Code by Michael Feathers. It's an entire book about getting code that doesn't have tests into a harness. Essentially, how to deal with what you're talking about. It's worth every penny.
This post is a good reminder that you can start off simple and grow into a framework as you need it. Unit testing is ultimately about the tests themselves, and this gives you the straightest path to writing them.
That is exactly my intention. Thanks! :)
This is a good starting point! I'm not an expert on this topic, but unit testing seems easy when you test "pure" functions. What happens when you have different results for the same input? Or functions that just call functions and you have to start using spies? Would be great if you go deeper in those topics!
You implement it as a proper state machine and test each state independently. The vending machine Kata is a great example of just that. Man, that one drove me up the wall. I'd much prefer to just have functions that always return the same output given the same input.
github.com/rubberduck203/VendingMa...
Great question!
Exactly! If you want to make your life as easy as possible, you'll have as high a percentage of pure functions as possible. This requires explicit effort.
While you can achieve purity in any language, functional programming languages let you achieve it far more easily.
Personally most side-effecting code I write is also externally facing code, which makes them Integration Tests rather than Unit tests.
I very rarely use spies/mocks and the like. Brittle.
I have a feeling you've got quite a lot more to say about this area than you let on in this post. It'd be quite hard for anyone to gain anything this post , on the one hand they'd have to understand how assertions fit into the bigger picture of unit testing in medium to large scale apps whilst on the other hand they don't have anything but a few simple assertion statements to work with - which they probably already knew in their day to day language
Good comment, your assessment is accurate! I'm however not sure how to express it better though.
I have another article coming on Medium related to building things incrementally though, should be out today or tomorrow.
Not only simple but with functionality like Live Unit Testing (Visual Studio 2017) is also awesome.
This makes me think that I should list a few unit testing frameworks, to make the article more complete.
Don't worry. The article is complete enough. ;)
XUnit! Moq! TestDouble! Karma! Jasmine!
hunh. To me this is by far the hardest part of development. I'm not a specialist though. Different strokes for different folks I guess
Why is that the case for you? I'd like to find out so I can solve your problem and make testing better.
I rarely ever have to debug or println any more - just use a test.
Well, for me it's the tooling. Getting everything to work together. The point of your article seems to be not using things like Mocha or what-have-you but I personally would be afraid to test without it. Or rather, I have no idea how I would use your method day-to-day.
unit testing is far more complex than writing an assertion statement... this post is pointless :/
What do you have in mind? I'd like to know to improve the post :)
I do use test frameworks extensively and find people have severe resistance to testing because of the learning curve. So this is a simple alternative for the newbie.
What you define as "unit test" in your post are actually mere assertions. They are helpful to ensure the sound state of your internal code but not for documenting and ensuring that your code behaves correctly as expected (as in a unit test). So, I'm not saying that assertions in code are useless, I'm saying that it's not unit testing! moreover an unit test has usually multiple assertions because the goal is to thest a method (the unit) under different scenarios!
what is the name of that syntax color pallete?
That's so awesome! I didn't know about the Bash or Node ones. I've wanted to have some kind of testing for some of my Bash scripts. Thanks for sharing!