Default arguments for cooler testing
In my other post about
creating properly typechecked helper functions in Typescript, I missed something out from the
examples that's present in the real code: default parameters! There's a function, createEvent
,
that returns an object with a dynamically generated UUID value in it. It also includes the current
timestamp. This is great from the programmer's point of view as there's no need to wire up these
values manually, but it sucks for unit tests because the values always change! The solution is to
use
default parameters
to keep the ergonomics of a clean public API, but still support unit testing and special cases
gracefully.
Let's say we have the following function:
;
Pretty standard. Calling it is ergonomic:
;
Great, my ground-breaking code goes live yet again. Or would if I had any unit tests! Fine. Bloody review process.
Unit testing
Unit testing this function should be pretty easy, right? I'll just write something like this (using Mocha and Chai):
"Create user",;
A pretty contrived test, but good enough for the purposes of this post. Naturally, it fails.
Because UUIDs are unique, the value returned by createUser
will be different every time, failing
our expect()
equality check. Similarly, the timestamp returned is always the current timestamp
so that's of no use to compare against either.
One solution to this problem is to only check that some fields are returned, perhaps just name
and email
, but this makes the unit test even less useful. Let's fix it properly by using default
parameters in our createUser
function:
;
When the function is called normally as createUser(name, email)
, _uuid
and _timestamp
are left
undefined, so use their default values as used. These are v4
(from uuid
) and a function that
returns the current timestamp. This doesn't change the normal interface to this function, however
now we can do much nicer stuff with our test:
"Create user",;
The above changes hardcode the id
and created_at
values, allowing the test to do a deep
comparison on the whole returned object. This simplifies the test code, and improves coverage by
ensuring that every field returned is what we expect. Adding tests like this to a well-typed
Typescript codebase should lead to more robust code and fewer (hopefully no) errors in production.
Fingers crossed anyway.
Now someone can approve my pull request. Hooray!
Caveats
There's a somewhat major caveat to this method, and that's one of argument counts. What if I want to
add a third field password
to my handy createUser
function? That's easy enough, but now all the
existing code that uses createUser
will error out. The _uuid
argument is now being given a
string as input, which is of course not what we want it to do. Typescript will likely warn you in
this case, but maybe not. A solution I can think of is to pass all your arguments to the function as
an object with overridable functions defined after:
;
Now adding password
is as easy as extending the first arguments object, and the "hidden" test
helper arguments _uuid
and _timestamp
are left neatly alone. Personally I think this is safer
and easier to reason about than positional string arguments, so take this as just another reason to
pass objects as function arguments.