Testing for Web applications
Test Types
For Web application projects, three types of tests are implemented: Unit Tests, Integration Tests, and UI Tests.
Non-UI Tests
The purpose of these tests is to validate that each unit of the application code performs as expected.
Generally, these tests must cover all the application’s modules:
controllers
decorators
forms
helpers
jobs
models
presenters
reppsitories
services
-
serializers
(API-only applications) views
In addition, any integration with third-party services (e.g., Stripe API, Twilio API, etc) must also be thoroughly tested.
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.
Testing Methodology
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.
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())
})
})
})
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.
Fabricator(:customer) do
last_name { 'John' }
first_name { 'Doe' }
email { 'john@[email protected]' }
end
require 'faker'
Fabricator(:customer) do
last_name { FFaker::Name.last_name }
first_name { FFaker::Name.first_name }
email { FFaker::Internet.email }
end
Optimizations
- Create only necessary test data, e.g. If the pagination per page size is 10, then create only 11 test records to test it.
it 'returns 10 customers' do
Fabricate.times(30, :customer)
sign_in_user
get :index
expect(assigns(:customers).size).to eq(10)
end
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.
it 'returns the amount in currency format' do
transaction = Fabricate(:transaction, amount: '100.00')
expect(transaction.formatted_amount).to eq('฿ 100.00')
end
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.
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
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.
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
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.
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
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.
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.
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
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 the 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 the 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 the Cypress code conventions.