mock и stub - в чем разница?

6,261 views
Skip to first unread message

cris...@gmail.com

unread,
Nov 15, 2007, 11:37:58 AM11/15/07
to RubyOnRails to russian
Глядя на документацию, не вижу никакой разницы в фичах. Насколько
понял, разница только в терминологии?

Michael Klishin

unread,
Nov 15, 2007, 11:49:09 AM11/15/07
to ror...@googlegroups.com
Мок — имитация объекта, stub — заглушка
для метода. Корректнее сравнивать stubs &
expectations (should_receive в RSpec).
Первые выбрасывают исключение, если
метод не был получен с ожидаемыми
параметрами, вторые прощают — но если
метод вызван, они всегда возвращают
указанное значение.

В относительно новом RSpec stub-ы тоже
принимают параметры через with.

Классика на тему использования моков и
стабов:

http://martinfowler.com/articles/mocksArentStubs.html
MK

cris...@gmail.com

unread,
Nov 15, 2007, 12:16:12 PM11/15/07
to RubyOnRails to russian
Тоесть, получается, если я тестирую метод и внутри него вызывает
другой метод, то подделывая обращение к нему используется stub? А
mock, это если бы я передавал в этот метод объект, интерфейс которого
имитирует то, что будет дергать мой метод?

ЗЫ: спасибо за RR. Очень приятная библиотечка :)

ЗЗЫ: кто-то видел сабжевую книжку http://xunitpatterns.com ? Стоит ее
купить??

On 15 нояб, 18:49, Michael Klishin <michael.s.klishin.li...@gmail.com>
wrote:
> Мок -- имитация объекта, stub -- заглушка
> для метода. Корректнее сравнивать stubs &
> expectations (should_receive в RSpec).
> Первые выбрасывают исключение, если
> метод не был получен с ожидаемыми
> параметрами, вторые прощают -- но если

Alexey Verkhovsky

unread,
Nov 15, 2007, 12:42:54 PM11/15/07
to ror...@googlegroups.com
On Nov 15, 2007 10:16 AM, cris...@gmail.com <cris...@gmail.com> wrote:
> Тоесть, получается, если я тестирую метод и внутри него вызывает
> другой метод, то подделывая обращение к нему используется stub? А
> mock, это если бы я передавал в этот метод объект, интерфейс которого
> имитирует то, что будет дергать мой метод?

def test_stub_doesnt_care_to_be_called
foo = Object.new
foo.stubs(:bar)
# and never call foo.bar
end

def test_but_mock_does
foo = Object.new
foo.mocks(:bar)
# and never call foo.bar
end

Первый тест проходит, второй ломается со словами "foo должен был
получить .bar(), а не получил ничего".
Использовать все это можно многими способами, один из основных уроков
- не писать mocks() когда можно писать stubs().

--
Alexey Verkhovsky
CruiseControl.rb [http://cruisecontrolrb.thoughtworks.com]
RubyWorks [http://rubyworks.thoughtworks.com]

Michael Klishin

unread,
Nov 15, 2007, 1:02:01 PM11/15/07
to ror...@googlegroups.com
Если вы тестируете вызов метода, нужен
expectation. Если тестируете что-то совсем
другое, и просто создаете среду, в
которой ваши проверки происходили бы
изолированно, тогда вам нужен stub.
Например, те экшены, которые "подлежат"
аутентификации обычно изолируют
стабом на current_user (из restful authentication) и иже
с ним.

Вот как это выглядит у меня в хелперах,
которые я использую с RSpec:

module CommonControllerSpecHelper

# Simplest but most userful signin technique ever
def simple_signin(account = Universe.create_account(8))

controller.stub!(:current_account).
and_return(account)

end

# ...

end


С другой стороны, если вы проверяете
саму аутентификацию, вам, возможно,
стоит сделать expectation на Account.authenticate.

Что-то типа

require File.dirname(__FILE__) + '/../spec_helper'

describe SessionsController, 'serving request to /sessions with POST' do
before(:each) do

end

def do_post_to_create_action(params)
post :create, params
end

def post_by_user_who_passes_authentication
@user = Universe.create_account(10)

Account.should_receive(:authenticate).with("j...@satriani.com",
"stagepass").
at_least(:once).
and_return(@user)

controller.should_receive(:current_account).and_return(@user)

do_post_to_create_action :email => "j...@satriani.com", :password
=> "stagepass"
end

it "should redirect successfully signed in user" do
post_by_user_who_passes_authentication

response.should be_redirect
end

it "should redirect successfully signed in user to new feedback
page" do
post_by_user_who_passes_authentication

response.should redirect_to(new_feedback_message_url)
end


def post_by_user_who_doesnt_pass_authentication
Account.should_receive(:authenticate).with("j...@satriani.com",
"12345").
at_least(:once).
and_return(false)

do_post_to_create_action(:email => "j...@satriani.com", :password
=> "12345")
end

it "should show login page when sign in fails" do
post_by_user_who_doesnt_pass_authentication

response.should be_success
end

it "shoud render sessions/new template when sign in fails" do
post_by_user_who_doesnt_pass_authentication

response.should render_template("sessions/new")
end
end


On 15 нояб. 2007, at 19:16, cris...@gmail.com wrote:

> Тоесть, получается, если я тестирую
> метод и внутри него вызывает
> другой метод, то подделывая обращение
> к нему используется stub? А
> mock, это если бы я передавал в этот
> метод объект, интерфейс которого
> имитирует то, что будет дергать мой
> метод?
>

MK

cris...@gmail.com

unread,
Nov 16, 2007, 4:05:17 AM11/16/07
to RubyOnRails to russian
> Первый тест проходит, второй ломается со словами "foo должен был
> получить .bar(), а не получил ничего".
> Использовать все это можно многими способами, один из основных уроков
> - не писать mocks() когда можно писать stubs().

Судя по описанию я использую mock. Но все таки хотел бы, чтобы знатоки
глядя на код ниже сказали, что они думают.

По самому коду, отмечу, что для нормальной работы rr на подмене
приватного метода, пришлось сделать его для теста публичным, иначе rr
затирал приватный метод.

...
# use rr for mock and stub
it "should test method_that_neet_test" do
# stub or mock???
mock(Foo).call_private_method(5) { "fake string" }
# test part
Foo.method_that_need_test(5).should ...
end
...

class Foo
class << self
def method_that_need_test(par1)
var = call_private_method(par1)
...
end

private
def call_private_method(par)
# some logic, that i won't to fake
end
end
end

Michael Klishin

unread,
Nov 16, 2007, 4:19:05 AM11/16/07
to ror...@googlegroups.com
Давайте сюда нормальный тест, без
всяких Foo.should test_a_thing

Не думаю, что 20 строчек кода являются
для кого-то critical competitive advantage.

On 16 нояб. 2007, at 11:05, cris...@gmail.com wrote:
>>
> ...
> # use rr for mock and stub
> it "should test method_that_neet_test" do
> # stub or mock???
> mock(Foo).call_private_method(5) { "fake string" }
> # test part
> Foo.method_that_need_test(5).should ...
> end
> ...
>
> class Foo
> class << self
> def method_that_need_test(par1)
> var = call_private_method(par1)
> ...
> end
>
> private
> def call_private_method(par)
> # some logic, that i won't to fake
> end
> end
> end

MK

Michael Klishin

unread,
Nov 16, 2007, 4:34:19 AM11/16/07
to ror...@googlegroups.com
По поводу тестирования private методов:
если это действительно требуется,
умные люди лишь временно делают метод
публичным

http://blog.jayfields.com/2007/11/ruby-testing-private-methods.html

On 16 нояб. 2007, at 11:05, cris...@gmail.com wrote:
> По самому коду, отмечу, что для
> нормальной работы rr на подмене
> приватного метода, пришлось сделать
> его для теста публичным, иначе rr
> затирал приватный метод.

MK

cris...@gmail.com

unread,
Nov 16, 2007, 4:41:20 AM11/16/07
to RubyOnRails to russian
Тогда так:

module AssetManager
class << self
def update_anormal_asset( asset_id, blob )
asset = Asset.find asset_id
# update content-file
path = path_to_asset( asset_id )
File.open(path,"wb") do |f|
f.write blob
end
# update asset
asset.md5 = Digest::MD5.hexdigest( blob )
asset.save
end

private
# ! method that I want to fake
def path_to_asset( asset_id )
File.join( Project.root_path,
Project::Helper.relative_path_by_id(asset_id) )
end
end
end


#specs
describe AssetManager, "when update asset" do
before :all do
# set path_to_asset to public because stub on private method
redefine it and
# remove from module
AssetManager.module_eval do
class << self
public :path_to_asset
end
end
end

before :each do
fixtures :blue_assets
@pool = {1 => "test1...", 3 => "test2!"}

# setup path and clear file
@path_to_trash_asset = File.join POOL_DIR, "update_asset/
trash_asset"
File.delete(@path_to_trash_asset) if File.exist?
(@path_to_trash_asset)

# mock call to path_to_asset
mock(AssetManager).path_to_asset.with_any_args.twice
{@path_to_trash_asset}
end

def pool_each_with_action
@pool.each do |asset_id, blob|
AssetManager.update_asset asset_id, blob
asset = Asset.find asset_id
yield asset_id, blob, asset
end
end

it "should write new content to asset" do
pool_each_with_action do |asset_id, blob, asset|
# check trash_asset
File.open(@path_to_trash_asset, &:read).should == blob
end
end

# other its...
end

On 16 нояб, 11:19, Michael Klishin <michael.s.klishin.li...@gmail.com>
wrote:

cris...@gmail.com

unread,
Nov 16, 2007, 5:45:44 AM11/16/07
to RubyOnRails to russian
Спасибо за ссылку. Добавлю в свой spec_helper. А что насчет самого
теста? Он стаб или мок? =)

On 16 нояб, 11:34, Michael Klishin <michael.s.klishin.li...@gmail.com>
wrote:

Maxim Kulkin

unread,
Nov 16, 2007, 6:05:48 AM11/16/07
to ror...@googlegroups.com
On Friday 16 November 2007 13:45:44 cris...@gmail.com wrote:
> Спасибо за ссылку. Добавлю в свой spec_helper. А что насчет самого
> теста? Он стаб или мок? =)

Зря добавляете. Это не та техника, которая нужна в повседневной жизни. Да и
вообще, лучше обходиться без тестирования private методов.

cris...@gmail.com

unread,
Nov 16, 2007, 6:15:16 AM11/16/07
to RubyOnRails to russian
Чем плохо писать тесты для приватных методов?

Если следовать классической схеме разработки через тестирование, то
сначала идет тест, потом реализация. Отсюда следует, что приватный
метод не появится, пока я не напишу тест, который тестирует этот
приватный метод.

Например, если я не напишу тест на приватный метод, и подделаю к нему
обращение, то при изменении приватного метода при прогонке теста на
публичном, который его использует ошибок не будет.

On 16 нояб, 13:05, Maxim Kulkin <maxim.kul...@gmail.com> wrote:

Timur Vafin

unread,
Nov 16, 2007, 6:22:24 AM11/16/07
to ror...@googlegroups.com
cris...@gmail.com пишет:

> Чем плохо писать тесты для приватных методов?
>
> Если следовать классической схеме разработки через тестирование, то
> сначала идет тест, потом реализация. Отсюда следует, что приватный
> метод не появится, пока я не напишу тест, который тестирует этот
> приватный метод.
>
> Например, если я не напишу тест на приватный метод, и подделаю к нему
> обращение, то при изменении приватного метода при прогонке теста на
> публичном, который его использует ошибок не будет.

Поддерживаю. Тема уже обсуждалась тут.

Аргументы против: тестами должен быть покрыт интерфейс. Т.е. Ты меняешь
внутренную логику работы, а тесты продолжают работать.

Michael Klishin

unread,
Nov 16, 2007, 6:43:14 AM11/16/07
to ror...@googlegroups.com
тем что тестировать надо не методы, а
поведение системы. В результате все,
что угодно, появится, когда я захочу,
лишь бы я брался за реализацию
поведения после его описания спеками.

В общем случае это не требует детально
тестировать приватные методы, хотя
иногда это нужно и совершенно не плохо;
нету догм и правил при написании
тестов, а у кого они есть, тому привет
горячий.

Как бы я тестировал ваш случай с ассет
менеджером напишу как будет время
подробнее. Обрывками я только запутаю.

On 16 нояб. 2007, at 13:15, cris...@gmail.com wrote:

> Чем плохо писать тесты для приватных
> методов?
>
> Если следовать классической схеме
> разработки через тестирование, то
> сначала идет тест, потом реализация.
> Отсюда следует, что приватный
> метод не появится, пока я не напишу
> тест, который тестирует этот
> приватный метод.
>
> Например, если я не напишу тест на
> приватный метод, и подделаю к нему
> обращение, то при изменении
> приватного метода при прогонке теста
> на
> публичном, который его использует
> ошибок не будет.

MK

Maxim Kulkin

unread,
Nov 16, 2007, 6:44:51 AM11/16/07
to ror...@googlegroups.com
On Friday 16 November 2007 14:15:16 cris...@gmail.com wrote:
> Если следовать классической схеме разработки через тестирование, то
> сначала идет тест, потом реализация. Отсюда следует, что приватный
> метод не появится, пока я не напишу тест, который тестирует этот
> приватный метод.
>
> Например, если я не напишу тест на приватный метод, и подделаю к нему
> обращение, то при изменении приватного метода при прогонке теста на
> публичном, который его использует ошибок не будет.

Вы забываете самое главное: зачем писать приватный метод и тест для него ?!
Этот метод НИКОМУ не нужен.

Чаще всего, новый код пишется как часть какого-нть public метода, а только
потом, в процессе рефакторинга, - выделяется в private метод. И вот для
выделения его в private метод не нужен никакой новый тест.

Maxim Kulkin

unread,
Nov 16, 2007, 6:49:02 AM11/16/07
to ror...@googlegroups.com

А то что Вы говорите - это ситуация, когда есть какой-то очень умный класс с
очень маленьким интерфейсом. Для решения проблемы в таких ситуациях
рекомендую почитать матириалы на тему design for testability.

Чаще всего, лучше сделать чуть более толстый, но хорошо тестируемый интерфейс.
А иногда - даже пересмотреть дизайна этого куска системы и разбить один
плохотестируемый интерфейс на несколько хорошо тестируемых.

Michael Klishin

unread,
Nov 22, 2007, 5:20:46 AM11/22/07
to ror...@googlegroups.com
Как и обещал ранее, предложу свой
вариант. Так как ни API, ни назначения
модуля я не знаю детальнее чем по вот
этому кусочку кода, можете просто
использовать это как гайдлайн в
написании спек на такие случаи.
Надеюсь, будет полезно.

# First step, extract objects we describe and their possible states/
actions


describe AssetManager, "calculating path to asset" do
end

describe AssetManager, "updating abnormal asset" do
end

# Second step, specify what behaviour you expect
describe AssetManager, "calculating path to asset" do

it "should use root path"

it "should calculate path using id"

end

describe AssetManager, "updating abnormal asset" do

it "should update asset content"

it "should update md5 checksum"

end

# Step three, spec-run-implement-run-refactor-...

# We cannot instantiate modules so lets get away with the following
class AssetManagerStubClass
include AssetManager
end

describe AssetManager, "calculating path to asset" do

it "should use root path" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

# We prohibit one assertion per test here but this drives us to
accessor implementation
# and makes no harm (side effects) cause it is a simple unit test
#
# Please do not make multiple assertions in controller specs
@asset_manager.root_path.should == "~/dev/playground/ruby/specs/
asset_manager"

# Make sure it actually calculates path relatively to given root
directory
@asset_manager.path(2).should =~ /~\/dev\/playground\/ruby\/specs
\/asset_manager/
end

it "should calculate path using id" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

@asset_manager.path(4).should == "~/dev/playground/ruby/specs/
asset_manager/assets/1"
end

end

# Then remove duplication...


describe AssetManager, "calculating path to asset" do

before(:each) do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")
end

it "should use root path" do
# We prohibit one assertion per test here but this drives us to
accessor implementation
# and makes no harm (side effects) cause it is a simple unit test
#
# Please do not make multiple assertions in controller specs
@asset_manager.root_path.should == "~/dev/playground/ruby/specs/
asset_manager"

# Make sure it actually calculates path relatively to given root
directory
@asset_manager.path(2).should =~ /~\/dev\/playground\/ruby\/specs
\/asset_manager/
end

it "should calculate path using id" do
@asset_manager.path(4).should == "~/dev/playground/ruby/specs/
asset_manager/assets/1"
end

end

# Next behaviour

describe AssetManager, "updating abnormal asset" do

before(:each) do
@io_string = <<-SONG
When I find myself in trouble mother Mary comes to me
Speaking words of wisdom
Let it be
SONG
end

it "should update asset content" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

id = 3

lambda {
@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
}.should change(File.new(@asset_manager.path(id)), :mtime)
end

it "should update asset content (implementation specific spec)" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

@file_instance = mock("file")
@file_instance.should_receive(:write).with(@io_string) # we do
not care what it returns in this case so nil is fine

File.should_receive(:open).
with(@asset_manager.path(id)).
and_yield(@file_instance)

@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
end

it "should update md5 checksum" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

id = 3

lambda {
@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
}.should change(@asset_manager, :md5)
end

end

# Clean it up

describe AssetManager, "updating abnormal asset" do

before(:each) do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

@io_string = <<-SONG
When I find myself in trouble mother Mary comes to me
Speaking words of wisdom
Let it be
SONG
end

def id
3
end

it "should update asset content" do
lambda {
@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
}.should change(File.new(@asset_manager.path(id)), :mtime)
end

it "should update asset content (implementation specific spec, so
stinky I can't stand it)" do
@asset_manager = AssetManagerStubClass.new(:root => "~/dev/
playground/ruby/specs/asset_manager")

@file_instance = mock("file")
@file_instance.should_receive(:write).with(@io_string) # we do
not care what it returns in this case so nil is fine

File.should_receive(:open).
with(@asset_manager.path(id)).
and_yield(@file_instance)

@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
end

it "should update md5 checksum" do
lambda {
@asset_manager.update_asset(:abnormal, id,
StringIO.new(@io_string))
}.should change(@asset_manager, :md5)
end

end

MK

Reply all
Reply to author
Forward
0 new messages