DEV Community

William Narmontas
William Narmontas

Posted on

Unit testing is simple

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?

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
Enter fullscreen mode Exit fullscreen mode

If we run:

$ seq 1 5 | bash inc.sh
2
3
4
5
6
$ echo $?
0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
$ python inc.py
Traceback (most recent call last):
  File "inc.py", line 4, in <module>
    assert increment(2) == 3
AssertionError
$ echo $?
1
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode
$ node inc.js

assert.js:81
  throw new assert.AssertionError({
  ^
AssertionError: 2 == 3

...
$ echo $?
1
Enter fullscreen mode Exit fullscreen mode

Scala

Comes with assert.

def inc(n: Int) = n + 2
assert(inc(1) == 2)
Enter fullscreen mode Exit fullscreen mode
$ scala inc.scala
java.lang.AssertionError: assertion failed
$ echo $?
1
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
lennartb profile image
Lennart

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!

Collapse
 
scalawilliam profile image
William Narmontas • Edited

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".

Collapse
 
rubberduck profile image
Christopher McClellan

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.

Collapse
 
mortoray profile image
edAโ€‘qa mortโ€‘oraโ€‘y

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.

Collapse
 
scalawilliam profile image
William Narmontas

That is exactly my intention. Thanks! :)

Collapse
 
alancampora profile image
Alan Campora

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!

Collapse
 
rubberduck profile image
Christopher McClellan

What happens when you have different results for the same input?

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...

Collapse
 
scalawilliam profile image
William Narmontas

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.

Collapse
 
kylessg profile image
Kyle Johnson

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

Collapse
 
scalawilliam profile image
William Narmontas

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.

Collapse
 
fortegabbm profile image
ferOrtega

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

Collapse
 
scalawilliam profile image
William Narmontas

This makes me think that I should list a few unit testing frameworks, to make the article more complete.

Collapse
 
fortegabbm profile image
ferOrtega

Don't worry. The article is complete enough. ;)

Collapse
 
jevenson profile image
Josh Evenson

XUnit! Moq! TestDouble! Karma! Jasmine!

Collapse
 
theednaffattack profile image
Eddie Naff

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

Collapse
 
scalawilliam profile image
William Narmontas

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.

Collapse
 
theednaffattack profile image
Eddie Naff

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.

Collapse
 
daveoncode profile image
Davide Zanotti

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

Collapse
 
scalawilliam profile image
William Narmontas

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.

Collapse
 
daveoncode profile image
Davide Zanotti

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!

Collapse
 
miryamfv profile image
Anonymous

what is the name of that syntax color pallete?

Collapse
 
rpalo profile image
Ryan Palo

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!