41
INTRODUCTION TO UNIT TESTING WITH RSPEC by Artem Szubowicz

Introduction to unit testing

Embed Size (px)

Citation preview

INTRODUCTION TO UNIT TESTINGWITH RSPEC

by Artem Szubowicz

WHY TESTING?

JUNIOR DEVELOPER'S GUIDE1. write code2. write more code3. write event more code!4. run and check5. if it crashes - debug & goto 36. goto 1

WHY TESTING?

Writing unit tests does not give you profitimmediately.

Instead, it does give you great profit in thefuture.

WHY TESTING?

1. descriptive tests substitude documentation2. finding bugs/errors is much faster3. refactoring is safe4. stubbing units still allows you to write logic for them5. continuous integration

WHY TESTING?

JUNIOR DEVELOPER'S GUIDE(for those who tried testing)

1. write code2. write more code3. cover 100% of code with tests!4. run5. if it crashes - debug6. 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 GUIDE1. write code2. run3. if it crashes - debug & fix it4. cover it with tests5. goto 1

A GOOD UNIT TEST IS

CONSISTENTSame test, run with same code multiple times,

should always give same results.

A GOOD UNIT TEST IS

INDEPENDENTTest should not change any other objects,except of those, created in the test itself.

A GOOD UNIT TEST IS

DESCRIPTIVE

generated with --format documentation option

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 in the past.' end

context 'with date in future' do let(:date) { 'Sunday' }

RSPEC: DIRECTORY STRUCTURE

RSpec tests (called "specs") are usually placedin the spec/ directory with the samestructure, as app/ directory structure.

RSPEC: 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

factories and support directories are specific to RSpec

RSPEC: DIRECTORY STRUCTURE

RSpec will look for files, whose names endwith _spec.rb and run them.

RSPEC: TEST COMPONENTS

describesubjectletcontextitexpectcreate (FactoryGirl)

RSPEC: TEST COMPONENTS

Each test should start withRSpec.describe, to allow usage of all

other components.

RSPEC: DESCRIBE

Groups test cases and defines the type ofsubject 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 becalculated 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 subjectis 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 past.'

# 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 weekend.'

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 changesexternals outside the test, with a mock.

FACTORYGIRL

Allows to create mock objects. But requiresdescribing 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-THEN STRUCTURE# 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-THEN STRUCTURE# Given:let(:user) { create :user } let(:orders) { [ order1, order2 ] }

# When + Then: expect(user.place(orders)).to change(Order.count).by(2)

BEST PRACTICES

INFORMATIVE MESSAGESdescribe 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.count) end end

context 'with one order' do let(:orders) { [ create :order ] }

it 'places one order' do expect { subject }.to change(Order.today.count).by( end end

BEST PRACTICES

ONE ASSERTION PER `IT`

BEST PRACTICES

NO CONDITIONALSAll 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.count) end end

context 'with one order' do let(:orders) { [ create :order ] }

it 'places one order' do expect { subject }.to change(Order.today.count).by( end end

BEST PRACTICES

NO LOOPSReplace 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 CATCHINGTest should either expect an exception or no

exception to be thrown.

it 'throws ArgumentException' do expect { order_builder.place_order! }.to raise_exception(ArgumentErrorend

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:

conditionalsloopsexception handling

that means your tests need refactoring

BEST PRACTICES

SENIOR DEVELOPER'S GUIDE(Test Driven Development)

1. write marvelous tests2. write code3. run tests4. if they fail - fix the code5. goto 1

THE END