A project consists of 2 types of code. The first type is the code that implement business logic. For example code that checks a registration form submission and enters a record to the database. The second type code tests the first type of code and makes sure it behaves according to specification. This is automated testing.
Automated testing is not necessary to have. I most certainly prefer having preferably a full test coverage. But the final call is yours. I can code without testing. But before you opt out of testing I recommend you go through the rest.
Types of Tests
There are basically 2 types of tests. Unit tests and acceptance tests. Unit tests look in to individual application components in the point of view the developer. Acceptance tests has the point of view of an actor (a user who uses the system). With the 2 combined you have a good test solution that can give you peace of mind that your application behaves as it is intended to. This is because small changes in one location can break a feature some where else.
Ruby on Rails comes with Test Unit. Many prefer it, or it won't be bundled with Rails. I how ever prefer RSpec way of things and couple it with Capybara to have an end-to-end testing configuration.
Typical Test Configuration
My typical testing configuration for a Ruby on Rails project involves RSpec, Capybara backed by the Selenium web driver and factories powered by FactoryBot. With some additional configuration and custom helpers the combination can be used to provide an end to end testing solution.
Acceptance Tests
I use Capybara coupled with RSpec for acceptance testing. Consider the following example of an acceptance test suite written on a RSpec + Capybara configuration. The form has 3 inputs - name, email and password. We are going to test how the form operates in the perspective of the actor / user.
While there is a lot of code for a simple form, they are rather obvious code even you would understand. What is important to note is that the test engine can go through hundreds perhaps thousands of similar tests within a matter of minutes some thing not economically feasible to test manually with the help of a person.
# spec/features/visitor_signs_up_spec.rb
require 'spec_helper'
feature 'Visitor signs up' do
scenario 'with valid email and password' do
sign_up_with 'John Doe', 'valid@example.com', 'password'
expect(page).to have_content('Log In')
end
scenario 'with blank name' do
sign_up_with '', 'valid@example.com', 'password'
expect(page).to have_content('Name required')
end
# ....
scenario 'with invalid email' do
sign_up_with 'John Doe', 'invalid_email', 'password'
expect(page).to have_content('Email invalid')
end
scenario 'with blank password' do
sign_up_with 'John Doe', 'valid@example.com', ''
expect(page).to have_content("Password can't be blank")
end
def sign_up_with(name, email, password)
visit sign_up_path
fill_in 'Name', with: name
fill_in 'Email', with: email
fill_in 'Password', with: password
click_button 'Register'
end
end
How this works out is, the test engine will fire an invisible (headless) browser, open the page, and fille the fields. The above code would execute within a second. How long do you think it will take you to try the different scenarios?
Unit Tests
Unit testing is done on a component level in the perspective of the developer. This is done to assert that individual components meets their design expectations. Typically tests are written upfront and the business logic implemented last. When all tests for the component succeeds work on the component considered complete.
Rails comes pre-configured with Minitest. But I use RSpec for tests.
An example will be like:
require 'rails_helper'
RSpec.describe User, type: :model do
context 'factories' do
it { expect(build(:user)).to be_valid }
it { expect(build(:user, :admin)) .to be_valid }
it { expect(build(:user, :confirmed)).to be_valid }
end
let(:user) { build(:user) }
context 'validations' do
it 'invalidates disposable email addresses' do
user.email = 'test@fakemail.fr'
expect(user).not_to be_valid
expect(user.errors[:email].length).to be(1)
end
end
describe '#pre_process_email' do
it 'removes tags(+tag) from user name' do
user.email = 'test+user@example.com'
user.valid?
expect(user.email).to eq('test@example.com')
end
end
describe '#stripped_email' do
it 'removes tags' do
user.email = 'user+tag@example.com'
expect(user.send(:stripped_email)).to eq('user@example.com')
end
it 'removes periods' do
user.email = 'u.s.e.r@example.com'
expect(user.send(:stripped_email)).to eq('user@example.com')
end
end
describe '#with_similar_email' do
it 'returns users with similar email' do
user.email = 'test.user@example.com'
existing = create(:user, email: 'testuser@example.com')
expect(user.with_similar_email).to include(existing)
end
end
end
Unlike expectance testing that run on the browser, we do unit tests on the component. Because there is no need to run the whole app and open a browser such tests are wicked fast.
Factories
If we look at the last example, there are codes like build(:user)
, build(:user, :admin)
etc. What I am using are factory methods. First I am asking it for a vanilla user object. In the latter I am requesting for a specific trait/configuration of user :admin
.
Factory Bot (formally known as Factory Girl) is used to power factories.
FactoryGirl.define do
factory :user do
email { FFaker::Internet.email }
first_name { FFaker::Name.first_name }
last_name { FFaker::Name.last_name }
phone { FFaker::PhoneNumber.short_phone_number }
password 'password'
password_confirmation 'password'
admin false
trait :confirmed do
confirmed_at { Time.zone.now }
end
trait :admin do
admin true
end
end
end
Conclusion
Automated tests require a lot of code. Some components may require more code to test it than the amount of code that takes to implement it. How ever as you can see they are mostly english and usually is copy-paste and updated. Throughout the development process the time it to takes to write the tests would be dwarfed by the time it takes to manually test.
Manual tests can't test all aspects of the code either. For example assume a 3rd party service due to a bug in their end some of the time sends an unexpected response. How are you going to test the update? May be call them up and say "You know that error that happen from time to time, can you please trigger it for us to test an update?"?
If you want to have a reliable website / web application / API to be developed you should test. It doesn't take that much dev time. In fact, it might actually save time. How ever as I said the final call is yours.