INTRODUCTION TO UNIT TESTING
WITH RSPEC
by Artem Szubowicz
WHY TESTING?
JUNIOR DEVELOPER'S GUIDE
1. write code
2. write more code
3. write event more code!
4. run and check
5. if it crashes - debug & goto 3
6. goto 1
WHY TESTING?
Writing unit tests does not give you profit
immediately.
Instead, it does give you great profit in the
future.
WHY TESTING?
1. descriptive tests substitude documentation
2. finding bugs/errors is much faster
3. refactoring is safe
4. stubbing units still allows you to write logic for them
5. continuous integration
WHY TESTING?
JUNIOR DEVELOPER'S GUIDE
(for those who tried testing)
1. write code
2. write more code
3. cover 100% of code with tests!
4. run
5. if it crashes - debug
6. goto 1
WHY TESTING?
WHAT'S WRONG HERE?
do we need to test everything?
WHAT IS UNIT TESTING?
testing modules, classes and methods, written by us
WHY TESTING?
MIDDLE DEVELOPER'S GUIDE
1. write code
2. run
3. if it crashes - debug & fix it
4. cover it with tests
5. goto 1
A GOOD UNIT TEST IS
CONSISTENT
Same test, run with same code multiple times,
should always give same results.
A GOOD UNIT TEST IS
INDEPENDENT
Test should not change any other objects,
except of those, created in the test itself.
A GOOD UNIT TEST IS
DESCRIPTIVE
generated with --format documentationoption
emailValidator
isValid
for valid e-mail address
resolves with valid=true
for email address that is not valid
resolves with valid=false
resolves with correct reason
for error result
resolves with valid=false
resolves with correct reason
for service down
calls error with error code
isUnique
for non existing email
resolves with false
for existing email
resolves with true
RSPEC
RSpec is a framework for unit-testing in Ruby
RSPEC: CODE EXAMPLE
RSpec.describe OrderBuilder, type: :model do
let(:user) { create :user }
let(:dish) { create :dish }
let(:date) { random_day }
subject(:order_builder) { OrderBuilder.new(user, Date.parse(date)) }
describe '#initialize' do
context 'with date in the past' do
let(:date) { random_day_in_past }
it { expect { subject }.to raise_error(ArgumentError, 'Cannot place order i
end
context 'with date in future' do
let(:date) { 'Sunday' }
RSPEC: DIRECTORY STRUCTURE
RSpec tests (called "specs") are usually placed
in the spec/directory with the same
structure, as app/directory structure.
spec
├── controllers
│   ├── orders_controller_spec.rb
│   ├── restaurants_controller_spec.rb
│   ├── user
│   │   └── orders_controller_spec.rb
│   └── users_controller_spec.rb
├── factories
│   ├── orders.rb
│   ├── restaurants.rb
│   └── users.rb
├── models
│   ├── ability_spec.rb
│   ├── order_spec.rb
│   ├── restaurant_spec.rb
│   └── user_spec.rb
├── rails_helper.rb
├── spec_helper.rb
└── support
├── controller_helpers.rb
├── database_cleaner.rb
├── factory_girl.rb
└── request_helpers.rb
RSPEC: DIRECTORY STRUCTURE
factoriesand supportdirectories are specific to RSpec
RSPEC: DIRECTORY STRUCTURE
RSpec will look for files, whose names end
with _spec.rband run them.
RSPEC: TEST COMPONENTS
describe
subject
let
context
it
expect
create (FactoryGirl)
RSPEC: TEST COMPONENTS
Each test should start with
RSpec.describe, to allow usage of all
other components.
RSPEC: DESCRIBE
Groups test cases and defines the type of
subject being tested.
RSpec.describe OrdersController do
describe '#index' do
# ...
end
end
RSpec.describe OrderBuilder, type: :model do
describe '#initialize' do
# ...
end
end
adding a type for subject may add extra features
RSPEC: SUBJECT
Sets the subject being tested.
subject(:order_builder) { OrderBuilder.new(user, Date.parse(date)) }
# use `order_builder` variable
subject { -> { order_builder.place_order(order_params) } }
# use `subject` variable
RSPEC: LET
Defines a variable, whose value will be
calculated when being used.
let(:user) { create :user }
let(:dish) { create :dish }
# `random_day` method will be called when using `date` variable only
let(:date) { random_day }
# use `user`, `dish` and `date` variables
RSPEC: LET!
Defines a variable with a value immediately.
# `create :user` will be called right now
let!(:user) { create :user }
# use `user` variable
RSPEC: CONTEXT
Groups tests by the environment, test subject
is being used in. Used for the same subject, but
with different input/dependant values.
context 'with date in the past' do
let(:date) { random_day_in_past }
# here the `date` will equal to `random_day_in_past` call result
end
context 'with date in future' do
let(:date) { 'Sunday' }
# and here `date` will equal to `'Sunday'`
end
RSPEC: IT
Specifies test case' body.
it { expect { subject }.to raise_error(ArgumentError, 'Cannot place order in the
# `it` can also have a description
it "does not create order if user has one already" do
user.orders << create(:order, order_date: date)
expect(order_builder.order).to eq(user.orders.first)
end
RSPEC: EXPECT
Specifies test assertion.
expect { subject }.to raise_error(ArgumentError, 'Cannot place order at the weeke
expect { subject.method }.to change(Order.count).by(3)
# when used with `subject` method call:
# subject { order.dishes.count }
is_expected.not_to eq(0)
MOCKING OBJECTS
Stub any object, which potentially changes
externals outside the test, with a mock.
FACTORYGIRL
Allows to create mock objects. But requires
describing them in
spec/factories/FACTORY_NAME.rb
FACTORYGIRL: FACTORY
FactoryGirl.define do
factory :dish do |f|
f.name { Faker::Team.creature }
f.price { Random::rand(0 .. 100) }
f.description 'tasty dish'
f.kind :main_course
association :restaurant
end
end
FACTORYGIRL: CREATE
# uses factory named `dish`
let(:order) { create :dish }
UNIT TESTING BEST PRACTICES
BEST PRACTICES
GIVEN-WHEN-THENSTRUCTURE
# Given:
user.orders << create(:order, order_date: date)
# When:
order_builder.place_order!
# Then:
expect(Order.today.count).to eq(2)
BEST PRACTICES
GIVEN-WHEN-THENSTRUCTURE
# Given:
let(:user) { create :user }
let(:orders) { [ order1, order2 ] }
# When + Then:
expect(user.place(orders)).to change(Order.count).by(2)
BEST PRACTICES
INFORMATIVE MESSAGES
describe OrderBuilder do
describe '#place_order!' do
subject { -> { order_builder.place_order! } }
context 'with no orders' do
it 'places nothing' do
expect { subject }.not_to change(Order.today.coun
end
end
context 'with one order' do
let(:orders) { [ create :order ] }
it 'places one order' do
expect { subject }.to change(Order.today.count).b
end
end
BEST PRACTICES
ONE ASSERTION PER `IT`
BEST PRACTICES
NO CONDITIONALS
All the situations must be checked by test
cases.
describe OrderBuilder do
describe '#place_order!' do
subject { -> { order_builder.place_order! } }
context 'with no orders' do
it 'places nothing' do
expect { subject }.not_to change(Order.today.coun
end
end
context 'with one order' do
let(:orders) { [ create :order ] }
it 'places one order' do
expect { subject }.to change(Order.today.count).b
end
end
BEST PRACTICES
NO LOOPS
Replace them with (multiple) tests.
it 'sets "delivered" status for all orders' do
expect(user.orders.today).to all(eq(Order.DELIVERED))
end
it 'does not set "delivered" status for any order' do
expect(user.orders.today).not_to all(eq(Order.DELIVERED))
end
BEST PRACTICES
NO EXCEPTION CATCHING
Test should either expect an exception or no
exception to be thrown.
it 'throws ArgumentException' do
expect { order_builder.place_order! }.to raise_exception(ArgumentError
end
it 'does not throw any exception' do
expect { order_builder.place_order! }.not_to raise_exception
end
BEST PRACTICES
If you face any of these in your tests:
conditionals
loops
exception handling
that means your tests need refactoring
BEST PRACTICES
SENIOR DEVELOPER'S GUIDE
(Test Driven Development)
1. write marvelous tests
2. write code
3. run tests
4. if they fail - fix the code
5. goto 1
THE END

Introduction to unit testing

  • 1.
    INTRODUCTION TO UNITTESTING WITH RSPEC by Artem Szubowicz
  • 2.
    WHY TESTING? JUNIOR DEVELOPER'SGUIDE 1. write code 2. write more code 3. write event more code! 4. run and check 5. if it crashes - debug & goto 3 6. goto 1
  • 3.
    WHY TESTING? Writing unittests does not give you profit immediately. Instead, it does give you great profit in the future.
  • 4.
    WHY TESTING? 1. descriptivetests substitude documentation 2. finding bugs/errors is much faster 3. refactoring is safe 4. stubbing units still allows you to write logic for them 5. continuous integration
  • 5.
    WHY TESTING? JUNIOR DEVELOPER'SGUIDE (for those who tried testing) 1. write code 2. write more code 3. cover 100% of code with tests! 4. run 5. if it crashes - debug 6. goto 1
  • 6.
    WHY TESTING? WHAT'S WRONGHERE? do we need to test everything?
  • 7.
    WHAT IS UNITTESTING? testing modules, classes and methods, written by us
  • 8.
    WHY TESTING? MIDDLE DEVELOPER'SGUIDE 1. write code 2. run 3. if it crashes - debug & fix it 4. cover it with tests 5. goto 1
  • 9.
    A GOOD UNITTEST IS CONSISTENT Same test, run with same code multiple times, should always give same results.
  • 10.
    A GOOD UNITTEST IS INDEPENDENT Test should not change any other objects, except of those, created in the test itself.
  • 11.
    A GOOD UNITTEST IS DESCRIPTIVE generated with --format documentationoption emailValidator isValid for valid e-mail address resolves with valid=true for email address that is not valid resolves with valid=false resolves with correct reason for error result resolves with valid=false resolves with correct reason for service down calls error with error code isUnique for non existing email resolves with false for existing email resolves with true
  • 12.
    RSPEC RSpec is aframework for unit-testing in Ruby
  • 13.
    RSPEC: CODE EXAMPLE RSpec.describeOrderBuilder, type: :model do let(:user) { create :user } let(:dish) { create :dish } let(:date) { random_day } subject(:order_builder) { OrderBuilder.new(user, Date.parse(date)) } describe '#initialize' do context 'with date in the past' do let(:date) { random_day_in_past } it { expect { subject }.to raise_error(ArgumentError, 'Cannot place order i end context 'with date in future' do let(:date) { 'Sunday' }
  • 14.
    RSPEC: DIRECTORY STRUCTURE RSpectests (called "specs") are usually placed in the spec/directory with the same structure, as app/directory structure.
  • 15.
    spec ├── controllers │   ├──orders_controller_spec.rb │   ├── restaurants_controller_spec.rb │   ├── user │   │   └── orders_controller_spec.rb │   └── users_controller_spec.rb ├── factories │   ├── orders.rb │   ├── restaurants.rb │   └── users.rb ├── models │   ├── ability_spec.rb │   ├── order_spec.rb │   ├── restaurant_spec.rb │   └── user_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support ├── controller_helpers.rb ├── database_cleaner.rb ├── factory_girl.rb └── request_helpers.rb RSPEC: DIRECTORY STRUCTURE factoriesand supportdirectories are specific to RSpec
  • 16.
    RSPEC: DIRECTORY STRUCTURE RSpecwill look for files, whose names end with _spec.rband run them.
  • 17.
  • 18.
    RSPEC: TEST COMPONENTS Eachtest should start with RSpec.describe, to allow usage of all other components.
  • 19.
    RSPEC: DESCRIBE Groups testcases and defines the type of subject being tested. RSpec.describe OrdersController do describe '#index' do # ... end end RSpec.describe OrderBuilder, type: :model do describe '#initialize' do # ... end end adding a type for subject may add extra features
  • 20.
    RSPEC: SUBJECT Sets thesubject being tested. subject(:order_builder) { OrderBuilder.new(user, Date.parse(date)) } # use `order_builder` variable subject { -> { order_builder.place_order(order_params) } } # use `subject` variable
  • 21.
    RSPEC: LET Defines avariable, whose value will be calculated when being used. let(:user) { create :user } let(:dish) { create :dish } # `random_day` method will be called when using `date` variable only let(:date) { random_day } # use `user`, `dish` and `date` variables
  • 22.
    RSPEC: LET! Defines avariable with a value immediately. # `create :user` will be called right now let!(:user) { create :user } # use `user` variable
  • 23.
    RSPEC: CONTEXT Groups testsby the environment, test subject is being used in. Used for the same subject, but with different input/dependant values. context 'with date in the past' do let(:date) { random_day_in_past } # here the `date` will equal to `random_day_in_past` call result end context 'with date in future' do let(:date) { 'Sunday' } # and here `date` will equal to `'Sunday'` end
  • 24.
    RSPEC: IT Specifies testcase' body. it { expect { subject }.to raise_error(ArgumentError, 'Cannot place order in the # `it` can also have a description it "does not create order if user has one already" do user.orders << create(:order, order_date: date) expect(order_builder.order).to eq(user.orders.first) end
  • 25.
    RSPEC: EXPECT Specifies testassertion. expect { subject }.to raise_error(ArgumentError, 'Cannot place order at the weeke expect { subject.method }.to change(Order.count).by(3) # when used with `subject` method call: # subject { order.dishes.count } is_expected.not_to eq(0)
  • 26.
    MOCKING OBJECTS Stub anyobject, which potentially changes externals outside the test, with a mock.
  • 27.
    FACTORYGIRL Allows to createmock objects. But requires describing them in spec/factories/FACTORY_NAME.rb
  • 28.
    FACTORYGIRL: FACTORY FactoryGirl.define do factory:dish do |f| f.name { Faker::Team.creature } f.price { Random::rand(0 .. 100) } f.description 'tasty dish' f.kind :main_course association :restaurant end end
  • 29.
    FACTORYGIRL: CREATE # usesfactory named `dish` let(:order) { create :dish }
  • 31.
  • 32.
    BEST PRACTICES GIVEN-WHEN-THENSTRUCTURE # Given: user.orders<< create(:order, order_date: date) # When: order_builder.place_order! # Then: expect(Order.today.count).to eq(2)
  • 33.
    BEST PRACTICES GIVEN-WHEN-THENSTRUCTURE # Given: let(:user){ create :user } let(:orders) { [ order1, order2 ] } # When + Then: expect(user.place(orders)).to change(Order.count).by(2)
  • 34.
    BEST PRACTICES INFORMATIVE MESSAGES describeOrderBuilder do describe '#place_order!' do subject { -> { order_builder.place_order! } } context 'with no orders' do it 'places nothing' do expect { subject }.not_to change(Order.today.coun end end context 'with one order' do let(:orders) { [ create :order ] } it 'places one order' do expect { subject }.to change(Order.today.count).b end end
  • 35.
  • 36.
    BEST PRACTICES NO CONDITIONALS Allthe situations must be checked by test cases. describe OrderBuilder do describe '#place_order!' do subject { -> { order_builder.place_order! } } context 'with no orders' do it 'places nothing' do expect { subject }.not_to change(Order.today.coun end end context 'with one order' do let(:orders) { [ create :order ] } it 'places one order' do expect { subject }.to change(Order.today.count).b end end
  • 37.
    BEST PRACTICES NO LOOPS Replacethem with (multiple) tests. it 'sets "delivered" status for all orders' do expect(user.orders.today).to all(eq(Order.DELIVERED)) end it 'does not set "delivered" status for any order' do expect(user.orders.today).not_to all(eq(Order.DELIVERED)) end
  • 38.
    BEST PRACTICES NO EXCEPTIONCATCHING Test should either expect an exception or no exception to be thrown. it 'throws ArgumentException' do expect { order_builder.place_order! }.to raise_exception(ArgumentError end it 'does not throw any exception' do expect { order_builder.place_order! }.not_to raise_exception end
  • 39.
    BEST PRACTICES If youface any of these in your tests: conditionals loops exception handling that means your tests need refactoring
  • 40.
    BEST PRACTICES SENIOR DEVELOPER'SGUIDE (Test Driven Development) 1. write marvelous tests 2. write code 3. run tests 4. if they fail - fix the code 5. goto 1
  • 41.