Google+ Badge

Thursday, April 14, 2016

Let's Build Something: Elixir, Part 4 - Better Tests, TypeSpecs, and Docs

We left off with our first test case working, but less-than-ideal. Specifically, it's leaving the timestamp file it writes sitting on the disk, and in an inappropriate location (the root of our project). This is super lame, and we should fix that.

Enter ExUnit callbacks and tags! ExUnit allows us to pass configuration data into and out of our tests by means of a dictionary, usually referred to as the "context". We can make good use of this context data by way of setup callbacks and tags. These are described well in the docs, and we'll lean on their examples for what we need to accomplish here.

So our test is currently testing TimestampWriter's ability to... ya know... write timestamps. And it works great, other than leaving the temp file sitting in the root of our project. While we could just add some code to our test to explicitly handle this, a better (and less-repetitious) approach is to modify our overall test case to do some setup and tear-down tasks for us automatically!

First, remove the junk file leftover at stats_yard/tstamp_write.test, and then we'll add a setup callback to our test case that will force our writes to happen in an appropriate directory:

Here we're exercising a one-way communication from our test to the setup callback by way of a tag. What's happening here is that ExUnit will call our setup callback before execution of every test within our test case. By preceding a test definition with @tag, we are specifying that a context dictionary should be passed to our setup callback that contains a key-value pair of { cd: "fixtures" }This is mostly copy-pasta'd straight out of the ExUnit docs, but a bit of explanation can't hurt:
  • Line 2: We need to make sure our tests don't run in parallel since we're going to be switching directories. It sucks, but it's the nature of the beast
  • Line 4: Define our setup callback, which will be executed prior to every test that is run
  • Line 5: Check to see if our cd tag is present in the callback's current context dict. This is necessary because the same callback is executed for every test, but not every test will necessarily use this particular tag
  • Line 6-7: Store the current directory and switch to the directory specified in the context
  • Line 8: When the test exits (whether success or fail), switch back to our original directory
  • Line 14: Our handy-dandy tag for the test that immediately follows
Let's see if it works!

Nope! ExUnit apparently doesn't create directories for you. Oops. Easy fix, and again:

Much better. Now we should do some cleanup after the fact, because let's face it - no one wants temp files committed to their repo.

A quick rundown of the updates (slightly out of order):

  • Line 23: Add a `tempfile` tag to our test to indicate that we're going to (attempt) to write a transient file
  • Line 24: Make our test accept a dict argument called `context` (which will contain our `tempfile` key)
  • Line 25: To keep things DRY, refer to the context's value for :tempfile instead of repeating the filename explicitly
  • Line 9: When the test is done, check to see if a tempfile was specified for the test that's being set up
  • Line 10: Make sure the tempfile actually got written, otherwise Line 11 will blow up
  • Line 11: Call the "dirty" version of File.rm/1, just in case there are any weird permissions issues that prevent deletion of the file
So now we should be able to run our test, and see precisely zero remnants of it:

Perfect! Now we can write more tests in here and gain some nice organization and cleanup bits without having to provide anything beyond a couple of appropriate tags. (And after all that, yes, I do realize that a tempfile doesn't necessarily need to go into its own directory if it's just going to be immediately deleted. This just makes me feel better.)

To wrap up, let's make our GenServer's module and API a bit more legit with a typespec and docstrings:

The @moduledoc and @doc directives are pretty straightforward - wrap up your docstrings in the """ markers, and get yo' docs on. Keep in mind that the docs are in markdown, so you can (and really should) make them look pretty.

The @spec directive on Line 22 is simply a way to specify the types of the arguments our function can accept, an the type of value it will return. Easy stuff, and super helpful when we start looking into static analysis - it can help iron out a ton of bugs early on.

Next Time

Now that we've spent some time on some of the basics that we'll be seeing over and over again, the next post will get into more of the meat of our project and start doing stuff that's more interesting than writing a timestamp to a file. Specifically, we'll define the first iteration of our data format and figure out a way to represent that in code such that we can validate incoming requests for appropriate structure.

Until then, feel free to peruse the source for this post at: