TDD in Crystal with minitest.cr

Please note that this tutorial was written for Crystal 0.24.1 and minitest 0.3.6. I can’t be certain that these instructions will work for other versions of either.

Welcome back to my blog. Today we’re going to explore Test Driven Development(TDD) with the Crystal language. The Crystal compiler does include a fully featured spec library that was inspired by Rspec. However, I always preferred Minitest over Rspec in Ruby, so if you’re like me, you’ll be very pleased to know that there’s a minitest inspired Crystal library, appropriately called minitest.cr. So I’m going to show you how to use minitest.cr to create and run unit tests in your Crystal projects.

Creating The Project

First thing we’re going to do is create our project. Since we’ll be including a third party library in Crystal (also known as a Shard), we’ll be making use of the standard Crystal project structure. Luckily, the Crystal compiler has a built-in feature to create the project folder and necessary files. From the command line of the directory you want to create your project in, type the following command:

$ crystal init lib learning_minitest

Allow me to explain this for those of you who haven’t used this feature before. If you’re familiar with this command, feel free to skip to the next section.

The crystal command is the command for accessing the Crystal compiler, and its suite of built-in tools. In this case, we’re using the init tool to create our project. The lib command means that we’re creating a library. The other option is app, which is used for creating an application. After that, comes the directory(if not the current directory) and project name.

When run, this command will create a folder called learning_minitest and the project skeleton, complete with a git repository and .gitignore file.

Note: Make sure to use an underscore and not a dash in the project name. The Crystal compiler takes a dash to mean that you’re extending a library. In this case, that means you’ll get a module called Learning, and a module called Minitest within the Learning module. That’s not what we want. We just want one module called LearningMinitest.

Shard.yml

Third party libraries in Crystal are referred to as Shards. They’re very analogous to Ruby’s Gems. Following this analogy, Crystal uses a YAML file called shard.yml, which simultaneously fills the functions of Ruby’s gemfile and gemspec files. For those of you who haven’t worked with a shard file yet, I’ll briefly cover the features that are related to this tutorial. If you are familiar with the shard file, feel free to add the minitest shard (version 0.3.6), and move on to the next section.

The crystal init command we used earlier created a basic shard.yml file, which will look something like this:

name: learning_minitest
version: 0.1.0

authors:
 - Chris Larsen <clarsen@example.org>

crystal: 0.24.1

license: MIT

Now there’s a lot of information you can put into a shard file, but for our purposes, all we need to do is add a dependency. For other features, I recommend looking at the shard.yml specification page.

Right below where it says says crystal: 0.24.1, place the following lines:

development_dependencies:
   minitest:
     github: ysbaddaden/minitest.cr
     version: "0.3.6"

 

Shards accepts two different types of dependencies, regular and development, which are indicated by the line stating dependencies: or development_dependencies:, respectively. In this case, you’re not going to need your tests in production, so we’re declaring minitest as a development dependency.

Now that our dependency is declared, we need to actually download the files for it. The following command does that for us:

$ shards install

This will install the necessary files from the git repositories of all the dependencies listed in your shard.yml file. If you need to update the version of the shard you’re using, you can use the shards update command after updating the version number in your shard file.

Creating Tests

Since the creation of a Crystal project doesn’t give us a test folder, we’ll have to do that manually. Your test folder should reside in the top level directory of your project structure, just as it would in Ruby, and be named test. Within it, we’re going to need a file for our tests. In the interests of convention, I suggest ending the name of the file with _test.cr. In this case, I’m going with learning_test.cr. To start with, add the following lines to your file:

require "minitest/autorun"

require "/../src/learning_minitest.cr"

class LearningTest < Minitest::Test

end

The first require statement is part of the minitest library, which allows us to run the test methods that we’ll write. The second is the file in our own project that will house the class that we’re going to test. Lastly, the class, which is a subclass of Minitest::Test is where we put all our tests.

The minitest.cr library contains all the same life cycle hooks that the original minitest contained; before_setup, setup, after_setup, before_teardown, teardown, and after_teardown.

However, I must provide a quick word of warning. In Ruby, many developers use the setup methods to initialize an instance of the class. In Crystal, due to the compiler, it won’t work that way. Since the compiler doesn’t know that the setup methods will be called before every test, it treats any item initialized in those methods as a union type with nil.

This can cause all kinds of problems, so I don’t recommend doing it. The alternatives are to use memoization, as in:

def cat
  @cat ||= LearningMinitest::Cat.new
end

Or, you can define any instances to test right below the class declaration, and give them a default value, like so:

class LearningTest < Minitest::Test
  @cat = LearningMinitest::Cat.new

end

I prefer memoization, so let’s add those lines inside the class. In case you’re wondering, yes, we’re creating a Cat class.

Now, let’s create our first test. Simple, yet effective:

def test_cat_likes_petting
  cat.pet
  assert cat.happy
end

Since minitest.cr is a third party library, there aren’t any default tasks that run all your test files, so you run them manually, one at a time, like so:

$ crystal run test/learning_test.cr

All test methods must be prefaced with test_ in order to be recognized as test methods and run during this process. Running them now should give you an error:

Can't infer the type of instance variable '@cat' of LearningTest

@cat ||= LearningMinitest::Cat.new

This is because we haven’t yet created the class that we’re testing. So let’s do that now.

Creating Library

The crystal init command created a learning_minitest.cr file in our src directory. Right now, it looks like this:

require "./learning_minitest/*"

# TODO: Write documentation for `LearningMinitest`
module LearningMinitest
   # TODO: Put your code here
end

We’re going to create our Cat class within this module. We also need to add the pet method and happy read-only property. Remove the two # TODO statements and add the following lines after the module definition:

class Cat
  getter happy : Bool

  def initialize
    @happy = false
  end

  def pet
    @happy = true
  end
end

If you’re not familiar with the getter line there, that defines a boolean instance variable called happy, and also creates a getter method for it, thus making it read-only. You can use the same format to define write-only properties with setter, or read-write properties with property. The Bool keyword can also be replaced with any class name to define an instance variable of that type.

Now that our class is defined, along with the pet method and happy instance varible, we can run our test and expect a successful output.

.

Finished in 00:00:00.001435720, 696.5146407377483 runs/s

1 tests, 0 failures, 0 errors, 0 skips

This output is quite possibly the biggest difference between the Ruby minitest, and the Crystal version. Whereas Ruby gives you the number of runs and assertions, this one gives you the number of tests. So a test method with 5 assertions, will still output as 1 test.

Lifecycle Hooks

As stated earlier, the same lifecycle hooks exist as in the Ruby version, so let’s use one of those now. Add the following setup method to learning_test.cr:

def setup
  cat.name = "Pandora"
end

Now to test this:

def test_cat_has_adorable_name
  assert_equal "Pandora", cat.name
end

Which should result in an error. This we fix by adding a property to our Cat class, and assign a default value to it.

property name = ""

Yes, another feature of our instance variable declarations is the ability to assign a default value that the variable holds if it’s not assigned anything during the initialization process.

Now, both of our tests should pass.

..

Finished in 00:00:00.001404765, 711.8628382683224 runs/s

2 tests, 0 failures, 0 errors, 0 skips

There’s a lot more to minitest.cr, but most of it is exactly like the original Ruby minitest.

If your tests aren’t passing, or there’s anything you don’t understand about what we’ve done, you can either refer to the github repository for this project, or leave a comment below.

Many thanks to ysbaddaden for creating this shard, without which, this blog post would not have been necessary. The source code for the shard can be found here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s