Testing for Web applications

Hero image for 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.