Unit testing is simple

By William Narmontas,

This page can be edited on GitHub.

Original article at The Practical Dev.

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?

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:

Conclusion

Now you have no excuse for not writing at least a few tests in your code.

Comments

Lennart commented:

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!

William Narmontas commented:

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:

  • unit-tested functional code.
  • functional/acceptance-tested mutable/side-effecting code, which wires up the functional 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:

  • Approach it by repeatedly extracting as much purity from mutable parts as you can. You'll find there's a lot of code that does not actually need to be mutable.
  • Turn the left-over mutable parts into immutable parts.
  • Only do this incrementally and not in a single step. Small commits, small pull requests.

The cost of fixing is still less than rewriting from scratch. See Joel Spolsky's article about "Things you should never do".

ferOrtega commented:

Not only simple but with functionality like Live Unit Testing (Visual Studio 2017) is also awesome.

Alan Campora commented:

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!

William Narmontas commented:

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.

Kyle Johnson commented:

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

William Narmontas commented:

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.

Stein B. Johansen commented:

For function, I agree, but is this unit-test or integration testing?

If you run up your application, and test how the functionality works from an external application, I would not call it "Unit-test" (or as it should have been called: Class test).

You can debate that your function/microservice is a "unit", but I do disagree, especially if it comes with a web-server which most microservices do.

Davide Zanotti commented:

unit testing is far more complex than writing an assertion statement... this post is pointless :/

William Narmontas commented:

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.

Davide Zanotti commented:

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!

Eddie Naff commented:

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

William Narmontas commented:

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.

Eddie Naff commented:

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.

edA-qa mort-ora-y commented:

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.

Ryan Palo commented:

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!