Using tests to drive out the design of objects is an effective way to write code. By taking the perspective of a client, your objects will develop simple, and intuitive interfaces. In addition, the tests act as both documentation and an automated, regression test suite. In this post, we’ll outline a basic test-driven workflow for MVC based applications. Our testing won’t be too exhaustive, but the workflow can be extended with additional, finer-grained tests, as you become more comfortable with test-driving code.
Start With a Failing Integration Test
Our sample application will be a command-line based text translator. Given a file of text, the app should be able to translate it into various languages. We’ll package up the app using Ruby gems.
Our first feature will be to support translating English text to Pig Latin. We’ll use Cucumber to write an end-to-end integration test to capture this initial feature (specifically aruba, which adds command-line steps to Cucumber).
Running this feature gives us our first failure.
Let’s implement these steps using
aruba‘s command-line helpers.
First, we write out a file of English text (
aruba will put this file in a
tmp directory), then we execute a command to translate the file (a command that doesn’t yet exist), and finally we verify the output. Running the test again, tells us that the
translator command doesn’t exist.
Let’s create an empty executable file in order to get a different failure.
We can now move on to handling this request.
Implement a Minimal Controller
Our command-line app will use an MVC based design. The first step in MVC is handling a request. Let’s flesh out our ideal controller interface from the executable
Translator is our app’s namespace. We can use a module method to handle a Pig Latin request. ARGF is an io stream representing the file passed in from the command-line.
STDOUT act as the request and response. We’ll read from
ARGF and we’ll write to
Cucumber tells us to implement our controller.
Translator.pig_latin method fixes the
NoMethodError, moving us further along.
For simplicity, we didn’t design the interface of the controller using its own unit tests. Usually, controllers do all their work by delegating to other objects. Unit testing a controller typically requires stubbing/mocking these collaborating objects. These tests often end up being ugly and hard to understand and maintain.
However, unit testing controllers can be beneficial, e.g., testing edge-cases, or when your test suite is painfully slow due to too many comprehensive integration tests. But when you’re just starting out, don’t worry about it. As you become more comfortable testing, explore unit testing your controllers to see if it’s worth it.
Use the View to Drive Out the Model
With a basic Pig Latin request handling controller in place, we can now move on to the next piece of MVC, the view. The view represents the user interface. It gets its data from a model. By implementing the view first, we can design our ideal model interface.
Translator.pig_latin now asks a
Translator::PigLatinView object to render itself. The
Translator::PigLatinView object is created with some Pig Latin, which it renders onto an
io object (this is the response, specifically STDOUT, that was passed in from the
Again, we skipped unit tests. Views, like controllers, rely on a lot of objects. As a result, their unit tests typically require extensive stubbing/mocking, and will be similar to controller tests. Skip them for now, and instead rely on integration tests for coverage. Like controllers, when you become more comfortable with testing, explore unit testing views to see if they provide any value.
Cucumber tells us to implement our model.
Translator::PigLatin model will process the request (the file of English text) and translate it to Pig Latin.
Drop Down and Unit Test the Model
Although we skipped unit testing our controller and view, we should unit test the model. In MVC, the model represents domain logic, the most important part of an app; in our case, Pig Latin translation. A high-level integration test failure could mean there’s a problem in a controller, a view, or even a model. Model-level tests can help narrow down these failures, eliminating tedious debugging. They’re also useful in situations where there’s a change in domain logic but not in the user interface.
We can get this failure to pass with the following implementation (for Pig Latin translation, we’ll use the igpay_atinlay gem, which adds
Come Back Up to the Integration Test
After adding a lower-level model test, we can come back up to our failing integration test; which should now be passing.
Find What Works for You
This basic test-driven workflow works for any MVC based application, command-line or web. High-level integration tests coupled with lower-level model tests are a great way to get started. As test-driving code becomes second nature to you, explore unit testing your controllers and views. It’s important to try out various testing workflows to find out what works for you and your project.