Testing for a Web application project 🔎
Test Types
For Web application projects, three types of tests are implemented: Unit Tests, Integration Tests, and UI Tests.
Unit Tests
Unit testing is a way of testing a unit, the smallest piece of code of an application. The purpose is to validate that each unit of the application code performs as expected. Unit testing is done during the development of an application by the developers.
Generally, write unit tests for the following modules:
models
services
forms
serializers
decorators
presenters
helpers
Integration Tests
Integration testing is a way of testing to ensure that the different components of the application work well together, without the overhead of the actual app environment (e.g. The browser). These tests should assert at the request/response level: status code, headers, body. They are helpful to test permissions, redirections, what view is rendered, etc.
Write integration tests for controllers
(including Web and API controllers) and third-party services (e.g. Stripe API, Twilio API).
UI Tests
UI testing is a way of testing an application flow from start to end. UI testing aims to simulate the real user scenario and validate the system under test and its components for integration and data integrity.
Write UI tests for every application screen that users can visit.
Test Coverage
Test coverage is a measurement that helps developers to determine if they are testing everything they are supposed to. It allows checking the quality of application testing by finding areas that are not currently covered. Test coverage helps to develop tests in those areas, increasing the overall quality of the application test suite.
Unit tests
Maintain 100 percent test coverage for unit testing. Unit tests are small and isolated, and they are faster than integration tests and UI tests.
Integration tests
Maintain 100 percent test coverage for integration testing also. Integration tests are faster than UI tests but slower than Unit tests.
UI tests
Write UI tests for all features, as well as verify every application screen correctness. UI tests are slower than unit tests and integration tests. That is why it is preferred to restrict writing UI test cases for happy paths only.
Testing Strategy
General
- For each pull request change, the tests must be added to that same pull request.
- Treat test code as production code. This mindset will improve the readability, stability, and maintainability of the test suite.
Fixtures
- Test data (e.g. New test user record) must be cleaned up from the test database after the test run completes.
// Bad
var _ = Describe("User", func() {
Describe("#CreateUser", func() {
It("creates a user", func() {
user, err := models.CreateUser()
if err != nil {
Fail("Creating user failed: " + err.Error())
}
Expect(user).ToNot(BeNil())
})
})
})
// Good
var _ = Describe("User", func() {
Describe("#CreateUser", func() {
It("creates a user", func() {
user, err := models.CreateUser()
if err != nil {
Fail("Creating user failed: " + err.Error())
}
Expect(user).ToNot(BeNil())
})
})
AfterEach(func() {
TruncateTables("users")
})
})
- Prefer to use a fake data generator (e.g. Faker) for generating test data.
# Bad
Fabricator(:customer) do
last_name { 'John' }
first_name { 'Doe' }
email { '[email protected]@.com' }
end
# Good
require 'faker'
Fabricator(:customer) do
last_name { FFaker::Name.last_name }
first_name { FFaker::Name.first_name }
email { FFaker::Internet.email }
end
Optimizing
- Create only necessary test data, e.g. If the pagination per page size is 10, then create only 11 test records to test it.
# Bad
it 'returns 10 customers' do
Fabricate.times(30, :customer)
sign_in_user
get :index
expect(assigns(:customers).size).to eq(10)
end
# Good
it 'returns 10 customers' do
Fabricate.times(11, :customer)
sign_in_user
get :index
expect(assigns(:customers).size).to eq(10)
end
- Prefer to use non persisted test objects to speed up tests when possible.
# Bad
it 'returns the amount in currency format' do
transaction = Fabricate(:transaction, amount: '100.00')
expect(transaction.formatted_amount).to eq('฿ 100.00')
end
# Good
it 'returns the amount in currency format' do
transaction = Fabricate.build(:transaction, amount: '100.00')
expect(transaction.formatted_amount).to eq('฿ 100.00')
end
DRY-ing
- Create testing helper methods/functions to reduce repetition across multiple tests.
# Bad
describe 'GET #index' do
it 'returns http success' do
admin = Fabricate(:admin)
request.env['devise.mapping'] = Devise.mappings[:user]
sign_in(admin)
get :index
expect(response).to have_http_status(:success)
end
end
# Good
describe 'GET #index' do
it 'returns http success' do
sign_in_as_admin
get :index
expect(response).to have_http_status(:success)
end
end
# in the `/spec/support/devise_helpers.rb`
def sign_in_as_admin
admin = Fabricate(:admin)
request.env['devise.mapping'] = Devise.mappings[:user]
sign_in(admin)
end
- Each test must verify a single-use case.
# Bad
it 'renders index view' do
sign_in_as_admin
get :index
expect(response).to render_template :index
expect(response).to have_http_status :ok
end
# Good
it 'renders index view' do
sign_in_as_admin
get :index
expect(response).to render_template :index
end
it 'returns 200 status' do
sign_in_as_admin
get :index
expect(response).to have_http_status :ok
end
Stubbing and Mocking
- All network requests must be mocked/stubbed.
# Bad
it 'sends an SMS to the customer' do
customer = Fabricate(:customer)
# `service.call` makes a network request
service = SMSService.new(customer)
expect(service.call).to eq(true)
end
# Good
it 'sends an SMS to the customer' do
customer = Fabricate(:customer)
# `service.call` makes a network request
service = SMSService.new(customer)
# Mocks `call` method to prevent network request
allow(service).to receive(:call).and_return(true)
expect(service.call).to eq(true)
end
- Prefer to use an HTTP request/response recorder (e.g. VCR) whenever possible over manual mocking/stubbing when testing external network requests. An HTTP request/response recorder records the request/response for the first time a test is executed (stored as files) and then re-uses these recordings for future test runs. So the tests can be executed with stubbed external network requests with real requests/responses.
# Good
it 'sends an SMS to the customer' do
customer = Fabricate(:customer)
# `service.call` makes a network request
service = SMSService.new(customer)
# Mocks `call` method to prevent network request
allow(service).to receive(:call).and_return(true)
expect(service.call).to eq(true)
end
# Preferred
it 'sends an SMS to the customer' do
customer = Fabricate(:customer)
# `service.call` makes a network request
service = SMSService.new(customer)
# Record the response when this test run for the first time and re-use the response when test runs later
VCR.use_cassette('sms/send_sms_success') do
expect(service.call).to eq(true)
end
end
API
- Prefer using a JSON Schema matcher (e.g. json_matchers) to validate JSON API responses.
# Bad
it 'responds with user' do
user = Fabricate(:user)
authenticate_with_doorkeeper(user)
get api_v1_me_path
response = JSON.parse(response.body)
expect(response.dig(:data, :attributes, :email)).to eq(user.email)
expect(response.dig(:data, :attributes, :username)).to eq(user.username)
end
# Good
it 'responds with user' do
user = Fabricate(:user)
authenticate_with_doorkeeper(user)
get api_v1_me_path
response = JSON.parse(response.body)
expect(response).to match_response_schema('v1/users/user', strict: true)
end
Tools - Libraries
Prefer to use the following libraries for testing:
Ruby
-
RSpec
is a domain-specific language testing tool to test Ruby code. Check our Rspec code conventions. -
Capybara
is an acceptance testing framework for Ruby Web applications. -
Fabrication
is a Ruby object factory generator. -
VCR
is an HTTP request/response recorder for Ruby. -
Faker
is a fake data generator for Ruby.
Elixir
-
ExUnit
is a built-in unit testing framework for Elixir. Check our ExUnit code conventions. -
Wallaby
is an acceptance testing framework for Elixir Web applications. -
ExMachina
is a test data factory generator for Elixir. -
ExVCR
is an HTTP request/response recorder for Elixir. -
Faker
is a fake data generator for Elixir. -
Mimic
is a mocking library for Elixir.
GO
-
Ginkgo
is a BDD testing framework for Go. -
Gomega
is a matcher library for Go. -
go-vcr
is an HTTP request/response recorder for Go. -
Faker
is a struct data fake generator for Go.
JavaScript
-
Jest
is a testing framework for JavaScript. -
Cypress
is a UI testing framework for JavaScript Web applications. Check our Cypress code conventions.