Today I present to you:
Declarative unit testing!

Instead of writing a bunch of example usages of your code, just define the behaviour you expect it to have!

To test a Fibonacci function, instead of writing

assert fib(1) == 1
assert fib(2) == 1
assert fib(3) == 2
assert fib(7) == 13
assert fib(69420) == 576441346790001

You write instead:

def test_fib(n):
if n <= 2: return 1
return fib(n -1) + fib(n - 2)

Simple! Can't possibly go wrong! All your tests will pass instantly!

  • 3
    for n in range(3, 1000):
    assert fib(n) = fib(n-1) + fib(n-2)
  • 5
    I would like to add to this, you should always test the behavior of logic and mathematics as well

    assert true != false

    assert 2 * 2 == 4

    You never know when the laws of the universe suddenly stop working.
  • 0
    The most important property of unit test code is its maintainability, as the test itself isn't productive and has to be easy to read and alter. Most testing code also has to terminate in a reasonable time frame.
    So simple checks for "f(a) results in b" are actually the best choice in most cases. They are pretty easy to read and maintain.

    I would test a fibonacci function with inputs -1, 0, 1, 2, 3, 4, 10, 100, and the expected max input.
    Normally, such known-result checks are done like:
    for in, out in [
      [-1, FailureVal], [0, FailureVal],
      [1, 1], [2, 1], [3, 2]...
    ] do assertFunResult(f, in, out).
  • 1

    Serious addition to that:

    Also test for the output to weird input (what are the zeroth, -1st, 3.5st, INF Fibonacci numbers? Should those return null, throw an error, snap to some bound?)
  • 1
    I have -1 and 0 in my example and assumed use of common native types - so the compiler will refuse to accept a float instead of an int.

    But yes, if you do it in JavaScript or are too lazy to type-hint your PHP or Python code, you have to test for failure on wrong input too.
    That is best done by having generic test functions taking a function and testing it with various carefully (to get as much potential edge cases covered as possible) selected inputs of all types except the type it should accept. Fail when the tested function doesn't fail.
  • 1
    @Oktokolo This is why I often tell people that weakly typed languages require more work.

    I've worked on a giant Haskell project (>10 million lines) and giant PHP project (>30 million lines).

    The Haskell project has about 500 tests, and I would almost dare to claim that there are zero bugs. The PHP project has 160k tests, and to give you an idea of the amount of bugs... We pay $7500/m for Sentry.

    Not saying every project should be written in Haskell... But I do believe strong(ish) type systems should be more common.
  • 1
    Zero bugs in non-trivial software - i doubt it.
    But type systems indeed are there for a reason. Some actually are expressive enough to be used for specifying the type of acceptable data.

    I would love to see a dynamically typed language with a decent type system - so you could have both: Hastily written prototypes and properly engineered libs written in the same language.
  • 0
    @Oktokolo Statically typed languages with good type inference get close to that ideal balance in my opinion -- Rust if you want to be on the safe side, Kotlin/Swift if you want things to be a bit easier.

    Then there's things like Typescript of course, and while I personally prefer it over plain Javascript, it does still very much feel as an "not fully fleshed out afterthought type system" on top of a chaotic language.

    These days, Rust is my default for both personal and work projects. I was actually very surprised how you can write very decent & safe Rust code with a basic subset of the the language -- It's not that difficult to write a program that can be read, understood, adjusted and compiled by a PHP junior.
  • 1
    Rust seems to be the next language to learn.
    Is it feature-stable now?
  • 1
    @Oktokolo Depends on what you consider stable, but in general, I'd say it's quite stable since v1.0 was released in 2015.

    You can't use unstable features on stable/beta channel, you have to use nightly and annotate your code with feature flags.

    So for 99% of libraries you would add through Cargo, you can expect any abandoned package from 2015 to still work in a new project in 2025, unless the library specifically says it was made to use flagged features from nightly.

    There should be no Py2/Py3 ecosystem splits.
  • 0
    @Oktokolo I think clojure with spec is exactly what you’re searching.
Add Comment