Bereksperimen menggunakan mock test
Saat tulisan ini ditulis sebenernya saya masih bingung dengan perbedaan antara stub, mock dan fake.
Namun untuk terminologi-terminologi ini saya ambil simple saja.
Saya menyebut itu stub ketika anda mendeklarasikan output. Misalnya pada webmock anda mendefinisikan output pada enpoint tertentu dan output yang anda definisikan adalah apa yang di test.
Saya ambil konsep ini dari webmock. Kenapa pemrogram melakukan ini? mereka melakukan ini agar test mereka tetap independent. Tanpa koneksi online test akan tetap jalan sempurna.
Lalu dengan mock. Saya menyebut itu sebagai mock ketika anda ingin melakukan break dependencies dengan kelas atau objek. Pada pemrograman berorientasi kelas jarang sekali berdiri sendiri, biasanya untuk memproses sesuatu kelas akan berelasi satu dengan lainnya.
Namun terkadang, kita ingin mengetes suatu kelas secara terisolasi. Karena akan mendatangkan banyak mamfaatnya, seperti kode uji semakin cepat dan mungkin yang paling sering adalah: mengadakan semua dependensi yang dibutuhkan kelas yang ingin kita uji tak seelok yang dibayangkan.
Pada tulisan ini saya ingin berbagi bagaimana saya menggunakan mock test ketika membuat fitur seperti import data dari file excel.
Fitur import excel adalah fitur yang hampir ada disetiap projek yang saya kerjakan, baik untuk projek untuk perusahaan yang baru, atau untuk perusahaan yang sudah jalan. Karena fitur input lewat form untuk data yang banyak dapat membuat tangan admin menjadi lelah.
Fitur import yang ingin kita bahas disini sangatlah simple, yaitu fitur import untuk membuat data user yang kolomnya hanyalah dua yaitu: username, age.
Baik, mari kita mulai.
Langkah pertama: install dependencies
Pada projek eksperimen ini saya menggunakan rspec-rails
sebagai test framework dan roo sebagai library pengimportnya.
Silahkan tambahkan kode ini di Gemfile
:
gem 'roo', '~> 2.8.0'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: %i[mri mingw x64_mingw]
gem 'capybara'
gem 'pry-rails'
gem 'rspec-rails'
end
Lalu install rspec dengan perintah bundle exec rails rspec:install
.
Langkah kedua: membuat system test-nya.
Karena kita akan menggunakan paradigma test-driven development, maka kita menulis kode testnya terlebih dahulu sebelum kode produksi.
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Import Users Spec', type: :system do
before do
driven_by :rack_test
end
context 'with valid params' do
it 'returns success message' do
visit new_employees_import_path
file = Rails.root.join('spec', 'fixtures', 'files', 'users-all-valid.xlsx')
attach_file :employee_import_form_file, file
click_on 'Submit'
expect(page).to have_content '3 employees has been created'
end
end
end
Kode test error sesuai ekspektasi kita, new_empoyees_import_path
dibaca sebagai undefined variable
.
Langkah ketiga: membuat kode produksi untuk membuat kode testnya sukses
Sekarang, mari kita membuat kode produksinya untuk membuat kode testnya success.
Pertama, register routes-nya terlebih dahulu:
# frozen_string_literal: true
Rails.application.routes.draw do
scope :employees, module: :employees, as: :employees do
resources :imports
end
end
Kedua, buat file controller-nya:
module Employees
class ImportsController < ApplicationController
def new
@form = EmployeeImportForm.new
end
def create
form = EmployeeImportForm.new(form_params)
form.save
flash[:success] = form.success_message
redirect_to new_employees_import_path
end
private
def form_params
params.require(:employee_import_form).permit(:file)
end
end
end
Kita akan menggunakan form object untuk fitur seperti ini. Kenapa form object? saya biasanya menggunakan form object bukan untuk form yang terdiri dari dua model atau lebih saja, namun untuk form yang tidak memiliki table-nya.
Maka, ketiga kita buat form object-nya:
# frozen_string_literal: true
class EmployeeImportForm
include ActiveModel::Model
include ActiveModel::Attributes
attr_reader :success_message
attribute :file, default: nil
def save
@success_message = '3 employees has been created'
end
end
Saya juga menggunakan ActiveModel::Attributes
untuk membuat variable input yang bisa memiliki fitur default value, setting type dan sebagainya.
Pada object ini saya awalnya tidak pusing untuk bagaimana bekerja dengan import-nya dulu, tapi saya buat fake implementation untuk melihat kode test kita sukses.
Lalu jalankan testnya kembali.
Dan testnya sukses.
Setelah fake implementation, sekarang kita sudah cukup bisa berkreasi untuk real implementationnya.
Untuk kodenya kira-kira bisa kita buat menjadi seperti ini:
# frozen_string_literal: true
class EmployeeImportForm
include ActiveModel::Model
include ActiveModel::Attributes
attr_reader :success_message
attribute :file, default: nil
def save
total_created = 0
xlsx = Roo::Spreadsheet.open(file)
xlsx.parse(headers: true).each_with_index do |row, index|
next if index.eql?(0)
User.create(username: row['username'], age: row['age'])
total_created += 1
end
@success_message = "#{total_created} employees has been created"
end
end
Lalu testnya kita jalankan kembali.
Dan testnya masih sukses.
Langkah terakhir: improve kode testnya
Saat ini kita masih mengetest pesan errornya saja, namun untuk apakah recordnya sudah tersimpan di database atau malah tidak tersimpan kita masih belum yakin.
Oke, untuk itu kita coba improve kode test kita yang sebelumnya untuk mengecek database.
Kira-kira menjadi seperti ini:
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Import Users Spec', type: :system do
before do
driven_by :rack_test
end
context 'with valid params' do
it 'returns success message' do
visit new_employees_import_path
file = Rails.root.join('spec', 'fixtures', 'files', 'users-all-valid.xlsx')
attach_file :employee_import_form_file, file
click_on 'Submit'
expect(page).to have_content '3 employees has been created'
expect(User.all.pluck(:username, :age)).to include(
['pquest', 23], ['larapel', 44], ['kimono', 31]
)
end
end
end
Oke, dan testnya masih sukses.
Namun, bagaimana menurut anda kode test yang diatas?
Saya biasanya tidak melakukan hal tersebut.
System spec biasanya saya buat hanya untuk menguji user interfacenya saja seperti bagaimana pengguna menginput data melalui form dan bagaimana flash message yang ditampikan ke user sebagai outputnya.
Sedangkan untuk low level seperti query database saya tidak uji di system spec, melainkan saya mengujinya di unit spec, seperti model spec, controller spec, forms spec, service spec, dll.
Pada kasus ini, yaitu form spec: employee_import_form_spec.rb
.
Sekarang mari kita biarkan system spec seperti yang awal kita buat, dan membuat form spec baru: employee_import_form_spec
.
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe EmployeeImportForm do
context 'with valid params' do
it 'returns success message' do
file_path = Rails.root.join(
'spec', 'fixtures', 'files', 'users-all-valid.xlsx'
)
form = EmployeeImportForm.new(file: file_path)
form.save
expect(form.success_message).to eq '3 employees has been created'
expect(User.all.pluck(:username, :age)).to include(
['pquest', 23], ['larapel', 44], ['kimono', 31]
)
end
end
end
Kode testnya menghasilkan pesan error:
1) EmployeeImportForm with valid params returns success mes
sage
Failure/Error: xlsx = Roo::Spreadsheet.open(file)
NoMethodError:
undefined method `=~' for #<Pathname:0x00005588a608e420>
# ./app/forms/employee_import_form.rb:13:in `save'
# ./spec/forms/employee_import_form_spec.rb:13:in `block (3 levels) in <top (required)>'
Finished in 1.44 seconds (files took 1.13 seconds to load)
3 examples, 1 failure, 1 pending
Failed examples:
rspec ./spec/forms/employee_import_form_spec.rb:7 # EmployeeImportForm with valid params returns success message
Ufff…
Oke, sekarang waktunya kita mengimplementasikan mock object.
Sebelum kita mengimplementasikan mock object, kita lihat dulu kode implementasi dari kode Roo::Spreadsheet.open(file)
.
Saya buka source-code-nya dan hasilnya:
require 'uri'
module Roo
class Spreadsheet
class << self
def open(path, options = {})
path = path.respond_to?(:path) ? path.path : path
extension = extension_for(path, options)
begin
Roo::CLASS_FOR_EXTENSION.fetch(extension).new(path, options)
rescue KeyError
raise ArgumentError,
"Can't detect the type of #{path} - please use the :extension option to declare its type."
end
end
def extension_for(path, options)
end
end
end
Hmnn, sepertinya class method dari open
mengekpektasikan objek yang mereka terima memiliki sebuah method path
.
Oke, waktunya oprek kode test kita kembali dan saya membuatnya menjadi seperti ini:
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe EmployeeImportForm do
context 'with valid params' do
it 'returns success message' do
file_path = Rails.root.join(
'spec', 'fixtures', 'files', 'users-all-valid.xlsx'
)
mock_excel = MockExcelFile.new(file_path)
form = EmployeeImportForm.new(file: mock_excel)
form.save
expect(form.success_message).to eq '3 employees has been created'
expect(User.all.pluck(:username, :age)).to include(
['pquest', 23], ['larapel', 44], ['kimono', 31]
)
end
end
private
class MockExcelFile
def initialize(path)
@path = path
end
def path
@path.to_s
end
end
end
Dan kode test-nya menjadi success.
:)
Sekarang ketika business code dari import menjadi kompleks kita bisa bebankan untuk cek query-query database-nya di form spec dan bukan di fitur system spec.
Untuk tulisan kali ini saya kira sudah cukup, semoga dapat membantu para pembaca skalian.
Terima kasih.