PHP to Ruby

Learning Ruby through PHP examples

Testing in Ruby with RSpec

May 31, 2018

Automating testing is one of the most important aspects to building trustworthy applications. Testing can seem like a tedious process, but your test suite becomes the bedrock of your development process. It’s an investment to devote write tests for a feature, but it pays in dividends when change inevitably happens.

Testing Forecaster in PHP

In this section, I’ll keep things simple and show you how to unit test a class in PHPUnit, then show you the Ruby equivalent with RSpec.

Let’s start with our OpenWeather API wrapper we made in the IoC Container example a few sections ago:

Running PHPUnit Tests

To run the test, simply run this command in the root directory of the project:

vendor/bin/phpunit tests

You should see that our 1 test passes.

The phpunit command will look for all tests located in the path you provide. In our case we passed the path to the tests directory. It found our test we created at tests/TestForecaster.php, executed the test and displayed the results in our terminal.

PHPUnit will treat every public method on a particular class extending the PHPUnit\Framework\TestCase.

Note: if you see an error about vendor/bin/phpunit does not exist make sure you’re in the right directory and make sure you have PHPUnit installed via composer.

What’s going on in the Forecaster Unit Test?

Whew! There’s a lot going on in our test example. We’re diving straight into mocking classes. You may have noticed there are 2 mocks in our example.

The first mock is straightforward, we need to mock Guzzle itself so we don’t actually make the API call to OpenWeather API:

In this series I’ll be showing how to write unit tests in Ruby with RSpec.

Installing RSpec

RSpec is just a gem, much like PHPUnit is a package. To install it in our Forecaster Ruby project we need to add it as a dependency for our test group:

bundle add rspec --grouptest

Note:* a “group” in a Gemfile is like defining an environment. In Composer we only have 2 groups available - “dependencies” which are for production and “dev-dependencies” which are used for development and testing. Bundler has several groups, such as “production”, “development” and “test”.

If all went well, then the contents of your Gemfile should resemble this:

Creating your First RSpec Unit Test

One nominclature difference between RSpec and other libraries, in RSpec terminology individual test cases are called specs.

So we always suffix our test files with _spec.rb.

In addition, the directory your individual specs live in is also called spec, it should have been created when you ran rspec --init.

Inside of this spec directory, create a new file called forecaster_spec.rb:

require'spec_helper'require'json'require_relative'../forecaster'RSpec.describeForecasterdoit'passes the given city as an argument to the API'docity='Alliance, OH'client=double(RestClient::Resource)allow(RestClient::Resource).toreceive(:new).and_return(client)expect(client).to(receive(:get).with({params: {q: city,appId: 'INSERT YOUR APP ID HERE'}}).and_return(JSON.generate(data: 'weather data here')))forecaster=Forecaster.newforecaster.weather_in(city)endend

Running Specs

Running specs is a breeze:

rspec

You should see that our 1 spec has passed! Woo!

Note: since spec is the default directory, you just need to run the rspec executable that was installed when you added it to your Gemfile.

What’s going on in our Spec?

So the whole point of this test was to verify that the city argument to forecaster.weather_in(city) would be passed to the OpenWeather API as q in the HTTP query.

Let’s take this spec apart and see what’s going on starting with the first line that defines the spec:

RSpec.describeForecasterdo# ...contents of spec inside of the blockend

We start off a spec as you can see by using Rspec.describe(Class). This describe method accepts a block and executes it.

It’s a little different from PHPUnit’s Class definition. Instead of inferring that all public methods in a class extending TestCase are tests, we specify them with it blocks:

RSpec.describeForecasterdoit'will run this block as a test'do# ...contents of spec inside of the blockendend

This it block will be executed as a single test. It leads to some very readable tests which is a huge benefit to your future self.

Asserting things

In PHPUnit, we’re used to making assertions as a method of the object:

Mocks in RSpec

RSpec has a few ways of mocking objects, here we’ll use a basic “double”. A double is very much like a mock.

The constructor argument is not actually required, but it’s helpful to pass the class of what you’re mocking to a double so it’s easier to read. You can create a double with just a string or pass nothing if you’d like:

# this will work perfectly finemock=double# this will also work perfectly finebird=double('mocking jay')

In our case we explictly passed the Class to the constructor of the double:

client=double(RestClient::Resource)

Stubbing vs Expecting on RSpec Mocks

This part was very confusing to me at first when I was learning RSpec testing. In PHPUnit and mocking libraries like Mockery, we don’t have this concept of allow vs expect.

allowallows you to stub a method.

So we know that our client instance’s get method will eventually be called with our parameters to the OpenWeather API:

Just like that, now whenever our RestClient::Resource creates a new instance, it will instead return our double.

It’s so easy to test all code in Ruby because of it’s flexibility. PHP’s requirement of passing dependecies via a constructor or setter argument in order to test encourages composed DRY code.

But not all PHP code is written with this in mind, this creates code that is sometimes impossible to unit test.

In Ruby, overriding a dependency instantiated in a constructor or even an instance or class method becomes trivial. You can refactor your code later, but at least you can test legacy code no matter the state it’s in.

Completing the Test

Now we have all the individual pieces we need to finish this unit test the Forecaster.weather_in(city) method: