Recently, our founder and CEO, Mikel Lindsaar spoke at the Ruby Developer Summit on Standard Development. He discussed the role of testing in project work, and talked about when you should follow the textbook approach and when it might be better to relax those rules. You can watch his talk on our youtube channel.
This blog post details my thoughts on how to approach writing tests. To illustrate, I’ll be using a Ruby/Rails example as Rails is a framework that embraces the culture of testing. However, I believe these concepts can be adapted to any language.
Testing plays - or at least should play - a key role in software development. Although it may not be obvious at the beginning, good test coverage saves you lot of pain in the future.
Here at reinteractive, we focus on producing bug-free and easily maintainable software. Our clients business depends on the code that we write and we take that responsibility very seriously.
It has been said that it is impossible to write 100% bug-free code. However, we should always aim for bug-free and - if we find a bug - it should be fixed as quickly as possible. Having a good test suite backing the code will go a long way in helping to achieve this.
There are a lot of fantastic resources on how to write good tests. However, when you get a real life project, you may find it difficult to apply some of those principles directly.
As an example, let’s say we have this ActiveRecord class:
```ruby # table_name -> users # columns # first_name -> string # last_name -> string class User < ApplicationRecord validates :first_name, presence: true validates :last_name, presence: true
def full_name “#{first_name} #{last_name}”.strip end end ```
It is a very simple class with an instance method called full_name
, which returns the concatenated string of first_name
and last_name
Let’s get started!
What is the Best Framework to Use?
If you are a newcomer to Rails, you will get asked this question a lot. And you have likely heard people bragging about how good the X
framework is. In the Rails community we have two major frameworks:
While a testing framework is undeniably useful, you could just create a standard Ruby file to test the code. Let’s take a step back and look at what we actually need to do.
The concept of Testing is basically:
you want to check if something returns the expected results for a known input
Therefore, the simple Ruby code below is a perfectly valid test for the full_name
method:
ruby
user = User.new(first_name: 'sylvester', last_name: 'stallone')
user.full_name == "sylvester stallone" #=> true
Remember that testing is a support function for you. Don’t spend too much time trying to learn a particular test framework. Go with the one you are most comfortable with. They all do the same (pretty much…), unless you have some specific requirements.
However, you should always consider the following when choosing a framework:
- How active the development is
This will give you some confidence on its reliability. Using an outdated test framework will cause you a lot of trouble further down the line. Given that almost all of them are open source and hosted on github, you can check for activity by looking at the date of the latest commit, how many open/closed tickets there are, the frequency of activities, etc..
- Check the community
By this, I mean, check if there are plenty of blog posts and/or stackoverflow questions around it. This shows that people are actually using it and, if you run into trouble, you will be able to find help.
In reality, when coming onto an existing project, you will have to use the testing framework that is already in use. However, you may be one of those lucky ones to get involved in the project in the very early stages and be involved in the decision of which framework to use :).
Oh…and by the way…don’t write tests in plain Ruby like the example above. That is a terrible idea! I was just using it to illustrate that testing is all about expecting known values by sending known values
.
Testing frameworks like Minitest and RSpec and built specifically for writing tests. They do one thing, and they do it well. The contributors for these frameworks have spent many hours making it useable so you don’t have to re-invent the wheel. Always use a test framework.
Pick a framework (if you have the opportunity to) that you are comfortable with. Don’t spend too much time worrying about their differences. In most cases, they all do the same thing.
TDD or Something Else
This is a very interesting topic. Almost all the TDD work I have seen has been in tutorials. I have yet to see a company that does 100% TDD on large projects.
I am personally not a hardcore fan of any of those camps. Sometimes I write the test before I write the actual code. However, there are also times when I write the code first and then the test. The important part is that I have a test for my new feature.
Whether you write the test before or after doesn’t really matter, as long as it is there by the time you push your code to review and merge it to the main codebase.
You should have a test for your code when you do a pull request / merge your code to the main branch. It doesn’t matter if you do TDD, BDD or something else. Having a test is what matters.
What Extent to Test
This is a good question, because you can come up with many combinations and spend hours on testing a small method. Let’s use the full_name
method in our example:
ruby
def full_name
"#{first_name} #{last_name}".strip
end
I will personally check:
- the happy path (with both first and last names provided)
- possible error paths (when both first and last names are nil)
- one other test (where either first name only or last name only is provided)
This way, I’ve covered pretty much all the cases and I can move on. I don’t want to spend too much time testing all the edge cases. If I come across any edge cases / issues I can add a test for it later.
Having said that, the code in the above example is very simple and not at all critical. If you are working on a critical piece of code that would cause major issues if it fails, it’s better to spend some extra time covering it thoroughly with a good test suite.
In the above case, my tests will look like this:
```ruby # rspec example # spec/models/user_spec.rb describe “#full_name” do let(:user) { User.new(first_name: first_name, last_name: last_name) }
context "with first name and last name" do
let!(:first_name) { 'sylvester' }
let!(:last_name) { 'stallone' }
it "shows the full name" do
expect(user.full_name).to eq('sylvester stallone')
end
end
context "when both first and last names are nil" do
let!(:first_name) { nil }
let!(:last_name) { nil }
it "shows the full name" do
expect(user.full_name).to eq('')
end
end
context "when only one name is present" do
let!(:first_name) { 'sylvester' }
let!(:last_name) { nil }
it "shows the full name" do
expect(user.full_name).to eq('sylvester')
end
end end ```
If it is not a critical code, test: - happy path - one worse case scenario (like when receiving nil values) - one other case. You can always add more tests later, when you find edge cases.
Don’t Test Framework / Libraries
This is a common thing I have seen in many code bases: Do not spend time adding tests around the features that are provided by the framework / libraries. (assuming you are using a popular framework/ library, like Rails). Chances are they already have tests around their features, and they have been battle-tested over time. There is no point spending time doing the same thing again.
Let’s go back to our example:
ruby
class User < ApplicationRecord
validates :first_name, presence: true
validates :last_name, presence: true
# ...
end
Say we want to test the validation for the first_name. Instead of doing:
ruby
# spec/models/user_spec.rb
describe '#validations' do
let(:user) { User.new }
it 'should validate presence' do
user.valid? # run validations
user.errors[:first_name].should include("can't be blank") # check for presence of error
end
end
end
do, (NOTE: I use shoulda gem for this):
ruby
# spec/models/user_spec.rb
describe '#validations' do
it { should validate_presence_of(:first_name) }
end
This way, you are checking that the validation is present (which ensures no one can remove it), but not the functionality of the validation. In most cases this approach will work, but not all cases.
Do not spend time testing framework features. Just check whether that particular feature is used in your code or not.
Always Write Readable Tests
Tests usually serve as documentation for your code. Try to write them in a way that someone can understand the intention of the code simply by reading your tests.
This is important because, with an existing project, the first thing most of us will do is to run the test suite to see if it passes. When working on a piece of code, if we can go to the respective test and understand what the code is supposed to do, it makes life much easier.
If you are using RSpec, you can use the following command to print the documentation while running your specs:
rspec spec --format documentation
The following spec:
```ruby # spec/models/user_spec.rb require ‘rails_helper’
RSpec.describe User, type: :model do
describe “#full_name” do let(:user) { User.new(first_name: first_name, last_name: last_name) }
context "with first name and last name" do
let!(:first_name) { 'sylvester' }
let!(:last_name) { 'stallone' }
it "shows the full name" do
expect(user.full_name).to eq('sylvester stallone')
end
end
context "when both first and last names are nil" do
let!(:first_name) { nil }
let!(:last_name) { nil }
it "shows the full name" do
expect(user.full_name).to eq('')
end
end
context "when only one name is present" do
let!(:first_name) { 'sylvester' }
let!(:last_name) { nil }
it "shows the full name" do
expect(user.full_name).to eq('sylvester')
end
end end
describe ‘#validations’ do it { should validate_presence_of(:first_name) } end end ```
will generate the documentation below:
A good practise when writing tests in RSpec is to use the documentation command above to check whether your spec is readable.
Try to write a readable test. That will help others (and also future you!) to understand what the code is doing.
In Conclusion
I hope you now have a basic idea on what tests to write, and how to write them without overdoing it. Remember, there is always a balance between productivity and maintainability.
As Mikel mentioned in his talk, by omitting tests altogether, you will initially get a boost to your productivity. However, it will be only be a short term win. Having a good test suite will give you the confidence to change the code over time without regression.
Happy coding!