今天会针对一个services
进行单元测试,并详述过程
首先先介绍基本的测试所安装的gem
# 测试
group :development, :testt do
gem 'rspec-rails', '~> 5.0.0'
gem 'factory_bot'
gem 'pry'
gem 'faker'
end
接着产rspec
档案
rails generate rspec:install
set_default_options has been deprecated, use set_options
Running via Spring preloader in process 79680
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
由於在我的专案中,test
已经被占用,所以用了另外一个名字testt
。
将产出的 spec/rails_helper.rb
的test
改成 testt
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
# https://github.com/rspec/rspec-rails/issues/70
ENV['RAILS_ENV'] = 'testt'
require File.expand_path('../config/environment', __dir__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# 引入所有 support 资料夹里面的档案
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
对应的 database.yml
default: &default
adapter: mysql2
host: 127.0.0.1
encoding: utf8
pool: 5
timeout: 5000
username: root
password: password
variables:
sql_mode: TRADITIONAL
development:
<<: *default
database: tungrp_dev
testt:
<<: *default
database: tungrp_test
设定完资料库设定之後,接着跑下列指令,将测试资料的资料库环境建立起来
rails db:migrate RAILS_ENV=testt
创建档案 ➡️ config/environments/testt.rb
,并且将原本在 config/environments/development.rb
的内容全部复制过去。
首先,我们使用 LongLiveRuby 文章进行测试
创建档案 ➡️ touch app/services/person.rb
创建档案 ➡️ touch spec/services/person_spec.rb
跑下列指令
bundle exec rspec ./spec/services/person_spec.rb
如果只想执行某一行的话可以指定行数
bundle exec rspec ./spec/services/person_spec.rb:4
目前要测试的档案为app/services/order/calculate_discount.rb
,以下为 app/services/order/calculate_discount.rb
的内容
# 计算刷退信用卡金额、购物金返还
#
# 范例: Order::CalculateDiscount.call(<order instance>, [{ variant_id: 1, quantity: 2 }])
class Order::CalculateDiscount
include OrderSyncDecorator
def self.call(order, return_items = nil, return_order = nil, order_price_total = nil, order_rebate_total = nil)
new(order, return_items, return_order, order_price_total, order_rebate_total).call
end
def initialize(order, return_items, return_order = nil, order_price_total = nil, order_rebate_total = nil)
# ......
end
def call
# ...
OpenStruct.new({
return_amount: (return_cash_amount + return_rebate_amount),
return_cash_amount: return_cash_amount,
return_rebate_amount: return_rebate_amount
})
end
attr_reader :variants, :order_items,
:customer, :order, :return_items, :items_total,
:order_price_total, :order_rebate_total, :order_total
private
# ...
end
我们使用OpenStruct
使得回传结果可以使用dot notation。在 Day5 我们提过 Openstruct
的概念,读者可以回去复习
接着我们创建档案来测试上面的services
➡️mkdir spec/services/order && touch spec/services/order/calculate_discount.rb
创建档案 ➡️ mkdir spec/support && touch spec/support/factory_bot.rb
RSpec.configure do |config|
# 加这行使得 FactoryBot 正常运行
FactoryBot.reload
config.include FactoryBot::Syntax::Methods
end
在 rails_helper.rb
写入require
,将factory_bot.rb
档案载入。
require './support/factory_bot.rb'
我们要在以下档案新增factory
spec/factories/*.rb
因此我们先创建商品假资料 ➡️ mkdir spec/factories && touch spec/factories/product.rb
再来编辑spec/services/order/calculate_discount.rb
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
let!(:customer) { create(:customer) }
describe '#initialize' do
context 'test factory_bot' do
before { customer }
it 'correct customer phone' do
expect(customer.phone).to eq('0983168969')
end
it 'incorrect customer phone' do
expect(customer.phone).not_to eq('0983168968')
end
end
end
end
接着执行指令
> bundle exec rspec ./spec/services/order/calculate_discount.rb
Finished in 0.55935 seconds (files took 5.69 seconds to load)
2 examples, 0 failures
let
只在被呼叫时才会被触发let!
等同於放在 before 内用实例建立测试资料。
create #=> 存取
build #=> 不存取
create_list #=> 存取多个
build_list #=> 不存取多个
先建立品牌,在建立商品
FactoryBot.define do
factory :brand do
end
trait :kenzo do
title { "KENZO" }
end
trait :agete do
title { "agete" }
end
end
若要商品跳过验证,可以这样写
FactoryBot.define do
factory :product do
title_zh { "汉汉特效药水" }
brand { create(:brand, :kenzo) }
collection_id { 1 }
sub_collection_id { 1 }
import_history_id { 1 }
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
let!(:kenzo) { create(:brand, :kenzo) }
let!(:customer) { create(:customer) }
# 创建商品
let!(:product) do
p = build(:product)
p.save(validate: false)
p
end
describe '#initialize' do
context 'test factory_bot' do
it 'correct product' do
expect(product.title_zh).to eq('汉汉特效药水')
end
end
end
end
这样写可以
FactoryBot.define do
factory :product do
title_zh { "汉汉特效药水" }
brand { create(:brand, :kenzo) }
collection_id { 1 }
sub_collection_id { 1 }
import_history_id { 1 }
end
trait :product_without_validations do
to_create { |instance| instance.save(validate: false) }
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建商品
let!(:product) { create(:product, :product_without_validations) }
describe '#initialize' do
context 'test factory_bot' do
it 'correct product' do
expect(product.title_zh).to eq('汉汉特效药水')
end
end
end
end
甚至这样写也可以
FactoryBot.define do
factory :product do
title_zh { "汉汉特效药水" }
brand { create(:brand, :kenzo) }
collection_id { 1 }
sub_collection_id { 1 }
import_history_id { 1 }
to_create {|instance| instance.save(validate: false) }
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建商品
let!(:product) { create(:product) }
describe '#initialize' do
context 'test factory_bot' do
it 'correct product' do
expect(product.title_zh).to eq('汉汉特效药水')
end
end
end
end
利用一个商品对应多个 variant
的概念,创建多个 variant
FactoryBot.define do
factory :variant do
sequence :id do |id|
id
end
price { 1600 }
product { create(:product) }
to_create {|instance| instance.save(validate: false) }
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
let!(:customer) { create(:customer) }
# 创建Variant
let!(:variants) { create_list(:variant, 25) }
describe '#initialize' do
context 'test factory_bot' do
it 'correct customer phone' do
expect(customer.phone).to eq('0983168969')
end
it 'incorrect customer phone' do
expect(customer.phone).not_to eq('0983168968')
end
it 'correct variant count' do
expect(variants.count).to eq(25)
end
end
end
end
> bundle exec rspec ./spec/services/order/calculate_discount.rb
Finished in 2.64 seconds (files took 4.01 seconds to load)
3 examples, 0 failures
FactoryBot.define do
factory :order do
customer
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建订单
let!(:order) { create(:order) }
end
我们在 before
提到的值必须为实体变数,下面的 it
区块才看得懂。
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建店面
let!(:store) { create(:store) }
# 创建Variant
let!(:variants) { create_list(:variant, 25) }
# 创建订单
let!(:order) { create(:order) }
# 创建Variant
let!(:variants) { create_list(:variant, 25) }
# 创建满额赠
let(:target_price_discount_first) { create(:target_price_discount, :scheme_1) }
let(:target_price_discount_second) { create(:target_price_discount, :scheme_2) }
# 创建订单项目
before do
@store_with_suffix = [store.title_zh, '_desu'].join
end
describe '#initialize' do
context 'test factory_bot' do
it 'correct variant count' do
expect(variants.count).to eq(25)
end
it 'correct store name with suffix' do
expect(@store_with_suffix).to eq('敦南店_desu')
end
end
end
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建店面
let!(:store) { create(:store) }
# 创建订单
let!(:order) { create(:order) }
# 创建25笔 Variant
let!(:variants) { create_list(:variant, 25) }
# 创建满额赠
let(:target_price_discount_first) { create(:target_price_discount, :scheme_1) }
let(:target_price_discount_second) { create(:target_price_discount, :scheme_2) }
# 创建订单项目
before do
@store_with_suffix = [store.title_zh, '_desu'].join
# 新增子订单
order.sub_orders.create(brand: store.brand, store: store)
end
describe '#initialize' do
context 'test factory_bot' do
it 'correct variant count' do
expect(variants.count).to eq(25)
end
it 'correct store name with suffix' do
expect(@store_with_suffix).to eq('敦南店_desu')
end
it 'correct sub_order count' do
expect(order.sub_orders.count).to eq(1)
end
end
end
end
DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :phone attribute in Customer model, pass `case_sensitive: true` option explicitly to the uniqueness validator.
因此在 customer.rb
的某一条验证改为
class Customer < ApplicationRecord
validates :phone , uniqueness: { case_sensitive: false }, presence: true
end
require './app/services/person'
require 'rails_helper'
RSpec.describe Order::CalculateDiscount do
# 创建店面
let!(:store) { create(:store) }
# 创建订单
let!(:order) { create(:order) }
# 创建25笔 Variant
let!(:variants) { create_list(:variant, 25) }
# 创建订单项目
before do
Order.class_eval do
define_method :used_point, -> { used_rebate + used_birth_gift }
end
# 新增满额赠
create(:target_price_discount, :scheme_1)
create(:target_price_discount, :scheme_2)
# 新增子订单
sub_order = order.sub_orders.create(brand: store.brand, store: store)
# 新增子订单项目
Rails.logger.debug '========= 创建订单项目 ========='
@items_number = rand(25)
order_items = (1..@items_number).map do |num|
sub_order.order_items.create! do |item|
item.variant = variants[num]
item.sold_price = variants[num].price
item.price = variants[num].price
item.order = order
item.quantity = 1
end
end
Rails.logger.debug '========= 算点数 ========='
price_hash = ::Order.count_price(order.order_items, order.used_rebate, order.used_birth_gift)
order.price = price_hash[:price]
if price_hash[:target_price_discount_title].present?
order.target_price_discount_title_zh = price_hash[:target_price_discount_title][:zh]
order.target_price_discount_title_en = price_hash[:target_price_discount_title][:en]
end
order.target_price_discount_value = price_hash[:target_price_discount_value]
order.original_price = price_hash[:subtotal]
order.save!
Rails.logger.debug '========= 退货 ========='
# 不用写 ...
Rails.logger.debug '========= 计算退货退款 ========='
return_items = order_items.map { |item| { variant_id: item.variant_id, quantity: item.quantity } }
@calculated_result = Order::CalculateDiscount.call(order, return_items)
end
describe '#call' do
context 'test factory_bot' do
it 'correct variant count' do
expect(variants.count).to eq(25)
end
it 'correct sub_order count' do
expect(order.sub_orders.count).to eq(1)
end
it 'correct order_items count' do
expect(order.order_items.count).to eq(@items_number)
end
# 商品价格=1600,满额赠一定不为0
it 'target price discount should not be zero' do
expect(order.target_price_discount_value.zero?).to be_falsey
end
it '实际退货金额=订单金额' do
expect(order.price).to eq(@calculated_result.return_cash_amount)
end
it '实际退还购物金=使用点数' do
expect(order.used_point).to eq(@calculated_result.return_rebate_amount)
end
end
end
end
当我们跑rails console
,只要跑 0 failures
就等於测试成功。
> bundle exec rspec ./spec/services/order/calculate_discount.rb
......
Finished in 6.72 seconds (files took 3.92 seconds to load)
6 examples, 0 failures
以上针对某个档案、某个功能进行测试的方式,我们称为单元测试。上述的测试需要假资料、测试资料内容,以及回传测试结果。测试的领域很大,除了上述提到的测试方式以外,还有许多种如整合性测试、测浏览器行为等。
测试的部分有很多可以分享,今天先介绍到这里。
>>: Progressive Web App 闲置中: Idle Detection API 空闲检测入门实做 (20)
初始设定 一开始我们就在 Azure 租一个 node , 程序後端(以下称API)、Nginx、M...
现在测试环境无法登入罗~ 该如何是好呢? 无法登入 先前可以使用测试环境与测试帐号登入,如Day3的...
嵌入式系统其实在我们的生活中无所不在,嵌入式系统是嵌入式计算机系统的简称,也就是说嵌入式系统与普通计...
AsyncTask非同步任务,或称异步任务,是一个相当常用的类别,是专门用来处理背景任务与UI的类别...
摇骰子 教学原文参考:摇骰子 这篇文章会介绍如何使用「当晃动发生」搭配「显示数字」与「随机取数」积木...