Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Clean Test Code

Clean Test Code

Rubyで読みやすいテストコードを書くためのコツについてまとめました

Shinichi Maeshima

May 18, 2017
Tweet

More Decks by Shinichi Maeshima

Other Decks in Technology

Transcript

  1. Clean Test Code
    @willnet

    View Slide

  2. ࣗݾ঺հ
    • લౡਅҰ aka @willnet or @netwillnet
    • ginza.rb ͔Βདྷ·ͨ͠
    • ϑϦʔϥϯεRailsٕज़ސ໰
    • https://github.com/willnet
    • https://twitter.com/netwillnet
    • http://blog.willnet.in

    View Slide

  3. ٕज़ސ໰ۀͬͯͳʹ΍ͬͯΔͷ
    Railsͷ஌ݟΛձࣾʹఏڙ͢Δ࢓ࣄ
    • ձࣾ૑ੈظʹෛ࠴ΛͨΊͯ͠·͕ͬͨͦΖͦΖฦ͍ͨ͠
    • ผͷݴޠͰ։ൃ͍͕ͯͨ͠Rubyʹ৐Γ׵͍͑ͨ
    • ελʔτ͔࣌Βෛ࠴ΛஷΊͣʹRailsΞϓϦέʔγϣϯΛ։ൃ
    ͍ͨ͠

    View Slide

  4. ۩ମతʹԿ΍ͬͯΔͷ
    • Rails΍γεςϜઃܭʹؔ͢Δ࣭໰ʹ౴͑Δ
    • ίʔυϨϏϡʔ
    • ࣾ಺ษڧձͷ։࠵
    • ίʔυΛॻ͘
    • ࠾༻ͷ૬ஊ৐Δ
    • etc

    View Slide

  5. Railsͷ஌ݟΛෳ਺ͷձ
    ࣾʹఏڙ࢝͠Ίͯ1೥൒
    ͘Β͍

    View Slide

  6. ৭ΜͳॴͰಉ͜͡ͱݴͬ
    ͯΔؾ͕͢Δͧʁ

    View Slide

  7. Α͘આ໌͢Δ಺༰Λ·ͱ
    ΊΔͱ…

    View Slide

  8. ʮ͜ΕಡΜͰ͓͍ͯͩ͘
    ͍͞ʯͰऴΘͬͯ΂Μ
    Γʂ

    View Slide

  9. Α͘આ໌͢Δ಺༰Λ·ͱΊͨྫ

    View Slide

  10. ࠓճ͸ςετίʔυͷॻ
    ͖ํʹ͍ͭͯ·ͱΊ·͠
    ͨ

    View Slide

  11. ͳͥςετίʔυ͔
    ΞϓϦέʔγϣϯίʔυʹ͸RubyͰՄಡੑΛอͭͨΊͷํ๏ɺ
    RailsͰՄಡੑΛอͭͨΊͷํ๏ͱ΋ʹॻ੶΍ωοτهࣄ͕͋Δ
    • 7 Patterns to Refactor Fat ActiveRecord Models
    • ΦϒδΣΫτࢦ޲ઃܭ࣮ફΨΠυ
    • RubyʹΑΔσβΠϯύλʔϯ

    View Slide

  12. ςετίʔυ
    ςετίʔυ΋ΞϓϦέʔγϣϯίʔυͱಉ͘͡୯ͳΔRubyͷί
    ʔυͳͷͰ͕͢ɺଟগؾΛ͚ͭͳ͍ͱμϝͳϙΠϯτ͕͋Δ
    ͔͠͠ॻ੶΍ωοτهࣄͰ͸͋·Γ৮ΕΒΕ͍ͯͳ͍

    View Slide

  13. ςετίʔυ͸௕͍

    View Slide

  14. ςετίʔυ(RSpec)͸
    ֊૚ߏ଄ʹͳ͍ͬͯΔ

    View Slide

  15. ςετίʔυ͸ந৅Խ͠
    ͮΒ͍

    View Slide

  16. ͡Ό͋Ͳ͏ॻ͘ͱ͍͍ͷ
    ͔

    View Slide

  17. લఏ
    • RSpec
    • FactoryGirl

    View Slide

  18. 1.͍ͨͩ͠֊૚ߏ଄Λ࡞
    Δ

    View Slide

  19. describeͱcontextΛਖ਼͘͠࢖͍෼͚Δ
    describeͱcontext͸ಉ͡ϝιου͕ͩɺ࣍ͷΑ͏ʹ࢖͍෼͚Δ
    ͜ͱͰԿΛςετ͍ͯ͠Δͷ͔ΛΘ͔Γ΍͘͢Ͱ͖Δɻ
    • describeͷҾ਺ʹ͸ςετͷର৅Λॻ͘
    • contextͷҾ਺ʹ͸ςετΛ࣮ߦ͢ΔͨΊͷ৚݅Λॻ͘
    ※describe, context, it ͷ֤Ҿ਺ʹ͖ͪΜͱͨ͠આ໌Λॻ͘
    ͷ΋௒େࣄ!!!

    View Slide

  20. # ྫ
    describe Stack do
    let!(:stack) { Stack.new }
    describe '#push' do
    context 'จࣈྻΛpushͨ͠ͱ͖' do
    before { stack.push('value') }
    it 'ฦΓ஋͕pushͨ͠஋Ͱ͋Δ͜ͱ' do
    expect(stack).to eq 'value'
    end
    end
    context 'nilΛpushͨ͠৔߹' do
    it 'ArgumentErrorʹͳΔ͜ͱ' do
    expect { stack.push(nil) }.to raise_error(ArgumentError)
    end
    end
    end
    describe '#pop' do
    # ུ
    end
    end

    View Slide

  21. describeͷ֎ʹςετσʔλΛஔ͔ͳ͍
    σʔλͷείʔϓΛߟྀͯ͠ɺςετέʔεʹෆཁͳσʔλ͕ଘࡏ
    ͢Δ͜ͱΛආ͚Δ

    View Slide

  22. # ҰݟDRYʹͰ͖ͦ͏ʹݟ͑Δ
    describe 'sample specs' do
    context 'a' do
    # ...
    end
    context 'b' do
    let!(:need_in_b_and_c) { ... }
    # ...
    end
    context 'c' do
    let!(:need_in_b_and_c) { ... }
    # ...
    end
    end

    View Slide

  23. # Ͱ΋͜͏มߋͯ͠͠·͏ͷ͸NG
    # ͦΕͧΕͷcontextͰඞཁͳσʔλͱඞཁͳ͍σʔλΛ೴಺ͰબΓ෼͚Δඞཁ͕ग़ͯ͘Δ
    describe 'sample specs' do
    let!(:need_in_b_and_c) { ... }
    context 'a' do
    # ...
    end
    context 'b' do
    # ...
    end
    context 'c' do
    # ...
    end
    end

    View Slide

  24. ྫ͚ͩͩͱʮผʹରͯ͠มΘΒͳ͘ͳ͍ʁʯͬͯࢥ͏͔΋͠Εͳ͍
    ͚Ͳɺ࣮ࡍͷςετίʔυ͸let΍context͕΋ͬͱେྔʹ͋ͬͯ
    Θ͚Θ͔Βͳ͍ͷͰɺෆཁͳσʔλ͕ͳ͍ঢ়ଶΛอ࣋͢ΔͷϚδେ
    ࣄͳΜͰ͢Α

    View Slide

  25. 2. ґଘΛݮΒ͢

    View Slide

  26. FactoryGirlͷσϑΥϧτ஋ʹґଘͤ͞ͳ͍
    FactoryGirlͷσϑΥϧτ஋͸͢΂ͯϥϯμϜ஋ʹ͢Δɻ
    ඞཁͳ஋ͷΈΛςετதͰ໌ࣔతʹࢦఆ͢Δ͜ͱʹΑΓɺʮ͜ͷς
    ετͰॏཁͳ஋͸ͳʹ͔ʯ͕Θ͔Γ΍͘͢ͳΔɻ

    View Slide

  27. ྑ͘ͳ͍ྫ
    FactoryGirl.define do
    factory :user do
    name 'willnet'
    active true
    end
    end

    View Slide

  28. describe User, type: :model do
    describe '#send_message' do
    let!(:sender) { create :user, name: 'maeshima' }
    let!(:receiver) { create :user, name: 'kamiya' }
    it 'ϝοηʔδ͕ਖ਼͘͠ૹΒΕΔ͜ͱ' do
    expect { sender.send_message(receiver: receiver, body: 'hello!') }
    .to change { Message.count }.by(1)
    end
    end
    end

    View Slide

  29. ্༙͖͕Δٙ໰
    • User#active #=> falseͷͱ͖͸Ͳ͏ͳΔʁ
    • User#nameͬͯઃఆ͢Δඞཁ͋Δͷʁ

    View Slide

  30. Α͍ྫ
    FactoryGirl.define do
    factory :user do
    sequence(:name) { |i| "test#{i}"}
    active { [true, false].sample }
    end
    end

    View Slide

  31. describe User, type: :model do
    describe '#send_message' do
    let!(:sender) { create :user }
    let!(:receiver) { create :user }
    it 'ϝοηʔδ͕ਖ਼͘͠ૹΒΕΔ͜ͱ' do
    expect { sender.send_message(receiver: receiver, body: 'hello!') }
    .to change { Message.count }.by(1)
    end
    end
    end

    View Slide

  32. ͜ΕͰ(ςετର৅ͷίʔυΛಡ·ͳͯ͘΋)࣍ͷ͜ͱ͕Θ͔Δ
    • User#name͕User#send_messageͷಈ࡞ʹӨڹ͠ͳ͍
    • User#active͕User#send_messageͷಈ࡞ʹӨڹ͠ͳ͍
    σϑΥϧτ஋͕มߋ͞ΕͨΒςετ͕ίέΔΈ͍ͨͳ͜ͱ΋ͳ͘ͳ
    Δ

    View Slide

  33. FactoryGirlͰbelongs_toҎ֎ͷؔ࿈Λσϑ
    ΥϧτͰ࡞੒͠ͳ͍
    has_manyͳͲͷؔ࿈ઌΛσϑΥϧτͷఆٛͰੜ੒ͯ͠͠·͏ͱڧ
    ྽ͳґଘؔ܎ʹͳͬͯ͠·͏ͷͰආ͚Δ

    View Slide

  34. ѱ͍ྫ
    FactoryGirl.define do
    factory :user do
    sequence(:name) { |i| "username#{i}" }
    after(:create) do |user, evaluator|
    create_list(:post, 2, user: user)
    end
    end
    end

    View Slide

  35. RSpec.describe User, type: :model do
    describe '#posts_ordered_by_popularity' do
    let!(:user) { create(:user) }
    let!(:post_popular) do
    post = user.posts[0]
    post.update(popularity: 5)
    post
    end
    let!(:post_not_popular) do
    post = user.posts[1]
    post.update(popularity: 1)
    post
    end
    it 'return posts ordered by populality' do
    expect(user.posts_ordered_by_popularity).to eq [post_popular, post_not_popular]
    end
    end
    end

    View Slide

  36. • after(:create)Ͱؔ࿈ઌΛੜ੒͍ͯ͠ΔͨΊɺؔ࿈ઌͷݸ਺
    ΍ଐੑ͕ઃఆͰ͖ͳ͍
    • ࠷ऴతʹͲΜͳσʔλ͕ἧ͍ͬͯΔͷ͔େมΘ͔Γʹ͍͘

    View Slide

  37. ྑ͍ྫ
    FactoryGirl.define do
    factory :user do
    sequence(:name) { |i| "username#{i}" }
    trait(:with_posts) do
    after(:create) do |user, evaluator|
    create_list(:post, 2, user: user)
    end
    end
    end
    end

    View Slide

  38. RSpec.describe User, type: :model do
    describe '#posts_ordered_by_popularity' do
    let!(:user) { create(:user) }
    let!(:post_popular) { create :post, user: user, popularity: 5 }
    let!(:post_not_popular) { create :post, user: user, popularity: 1 }
    it 'return posts ordered by populality' do
    expect(user.posts_ordered_by_popularity).to eq [post_popular, post_not_popular]
    end
    end
    end

    View Slide

  39. ೔࣌ʹ(ͳΔ΂͘)ґଘͤ͞ͳ͍
    ೔࣌ΛऔΓѻ͏ςετΛॻ͘৔߹ɺઈର࣌ؒΛ࢖͏ඞཁ͕ͳ͍έʔ
    εͰ͋Ε͹ͳΔ΂͘ݱࡏ೔͔࣌Βͷ૬ର࣌ؒΛར༻͢Δͷ͕ྑ͍ɻ
    ͦͷํ͕࣮૷ͷෆ۩߹ʹؾ͚ͮΔՄೳੑ͕૿͢ɻ

    View Slide

  40. # ѱ͍ྫ
    class Post < ApplicationRecord
    # όάؚ͕·Ε͍ͯΔ
    scope :last_month_published, -> { where(publish_at: (Time.zone.now - 31.days).all_month) }
    end
    RSpec.describe Post, type: :model do
    describe '.last_month_published' do
    let!(:april_1st) { create :post, publish_at: Time.zone.local(2017, 4, 1) }
    let!(:april_30th) { create :post, publish_at: Time.zone.local(2017, 4, 30) }
    before do
    create :post, publish_at: Time.zone.local(2017, 5, 1)
    create :post, publish_at: Time.zone.local(2017, 3, 31)
    end
    it 'return published posts in last month' do
    Timecop.travel(2017, 5, 6) do
    # ςετ͸௨Δ
    expect(Post.last_month_published).to contain_exactly(april_1st, april_30th)
    end
    end
    end
    end

    View Slide

  41. # ྑ͍ྫ
    # ઈରͰ͸ͳ͍͕ෆ۩߹ʹؾͮ͘Մೳੑ͕૿͢
    RSpec.describe Post, type: :model do
    describe '.last_month_published' do
    let!(:now) { Time.zone.now }
    let!(:last_beginning_of_month) { create :post, publish_at: 1.month.ago(now).beginning_of_month }
    let!(:last_end_of_month) { create :post, publish_at: 1.month.ago(now).end_of_month }
    before do
    create :post, publish_at: now
    create :post, publish_at: 2.months.ago(now)
    end
    it 'return published posts in last month' do
    expect(Post.last_month_published).to contain_exactly(last_beginning_of_month, last_end_of_month)
    end
    end
    end

    View Slide

  42. 3. લఏ৚݅ΛΘ͔Γ΍
    ͘͢͢Δ

    View Slide

  43. updateͰσʔλΛมߋ͠ͳ͍
    FactoryGirlͰ࡞੒ͨ͠ϨίʔυதͷΧϥϜΛupdateϝιου
    Ͱมߋ͢Δͱɺ࠷ऴతͳϨίʔυͷঢ়ଶ͕Θ͔Γʹ͘͘ͳΔ͠ɺς
    ετʹґଘ͍ͯ͠Δଐੑ΋Θ͔Γʹ͘͘ͳΔͷͰආ͚Δɻ

    View Slide

  44. # ѱ͍ྫ
    describe Post do
    let!(:post) { create :post }
    describe '#published?' do
    subject { post.published? }
    context 'when the post has already published' do
    it { is_expected.to eq true }
    end
    context 'when the post has not published' do
    before { post.update(publish_at: nil) }
    it { is_expected.to eq false }
    end
    context 'when the post is closed' do
    before { post.update(status: :close) }
    it { is_expected.to eq false }
    end
    context 'when the title includes "[WIP]"' do
    before { post.update(title: '[WIP]hello world') }
    it { is_expected.to eq false }
    end
    end
    end

    View Slide

  45. letΛ্ॻ͖͠ͳ͍
    ઌఔͷupdateͷ࿩ͱಉ͡ɻ֤ςετͷલఏ৚͕݅Θ͔ΓͮΒ͘ͳ
    ΔͷͰආ͚Δ

    View Slide

  46. # ѱ͍ྫ
    describe Post do
    let!(:post) { create :post, title: title, status: status, publish_at: publish_at }
    let(:title) { 'hello world' }
    let(:status) { :open }
    let(:publish_at) { Time.zone.now }
    describe '#published?' do
    subject { post.published? }
    context 'when the post has already published' do
    it { is_expected.to eq true }
    end
    context 'when the post has not published' do
    let(:publish_at) { nil }
    it { is_expected.to eq false }
    end
    context 'when the post is closed' do
    let(:status) { :close }
    it { is_expected.to eq false }
    end
    context 'when the title includes "[WIP]"' do
    let(:title) { '[WIP]hello world'}
    it { is_expected.to eq false }
    end
    end
    end

    View Slide

  47. 4. ந৅Խ͢Δͱ͖͸Α
    ͘ߟ͑ͯ

    View Slide

  48. shared_examples͸Α͘ߟ͑ͯ࢖͏
    shared_examplesΛར༻͢ΔͱίʔυͷॏෳΛ࡟আͰ͖Δ͕ɺॻ
    ͖ํʹΑͬͯ͸͔͑ͬͯՄಡੑΛམͱ͢͜ͱ͕͋Δɻ

    View Slide

  49. # ѱ͍ྫ
    # Ҿ਺ͱͯ͠౉͞Ε༵ͨ೔ʹରԠͨ͠෼͚ͩϙΠϯτΛ૿΍͢ϝιου
    # `Point#increase_by_day_of_the_week`Λςετ͍ͯ͠Δ
    RSpec.describe Point, type: :model do
    describe '#increase_by_day_of_the_week' do
    let(:point) { create :point, point: 0 }
    it_behaves_like 'point increasing by day of the week', 100 do
    let(:wday) { 0 }
    end
    it_behaves_like 'point increasing by day of the week', 50 do
    let(:wday) { 1 }
    end
    it_behaves_like 'point increasing by day of the week', 30 do
    let(:wday) { 2 }
    end
    # ...
    end
    end

    View Slide

  50. ͜Εݟͯͳʹ͕Ͳ͏ͳͬͯΔͷ͔͙͢ཧղͰ͖·͔ͨ͠ʁ

    View Slide

  51. ఆٛ͸ͪ͜Β
    RSpec.shared_examples 'point increasing by day of the week' do |expected_point|
    it "increase by #{expected_point}" do
    expect(point.point).to eq 0
    point.increase_by_day_of_the_week(wday)
    expect(point.point).to eq expected_point
    end
    end

    View Slide

  52. # ѱ͍ྫ(࠶ܝ)
    # - લఏ৚͕݅ଟ͍
    # - ఆ͕ٛ෼ࢄ͍ͯ͠Δ
    RSpec.describe Point, type: :model do
    describe '#increase_by_day_of_the_week' do
    let(:point) { create :point, point: 0 }
    it_behaves_like 'point increasing by day of the week', 100 do
    let(:wday) { 0 }
    end
    it_behaves_like 'point increasing by day of the week', 50 do
    let(:wday) { 1 }
    end
    it_behaves_like 'point increasing by day of the week', 30 do
    let(:wday) { 2 }
    end
    # ...
    end
    end

    View Slide

  53. # shared_examples Λ࢖ͬͨ··ՄಡੑΛଟগ૿ͨ͠ྫ
    RSpec.describe Point, type: :model do
    describe '#increase_by_day_of_the_week' do
    let(:point) { create :point, point: 0 }
    context 'on sunday' do
    let(:wday) { 0 }
    it_behaves_like 'point increasing by day of the week', expected_point: 100
    end
    context 'on monday' do
    let(:wday) { 1 }
    it_behaves_like 'point increasing by day of the week', expected_point: 50
    end
    context 'on tuesday' do
    let(:wday) { 2 }
    it_behaves_like 'point increasing by day of the week', expected_point: 30
    end
    # ...
    end
    end

    View Slide

  54. # ϒϩοΫҾ਺ΛύϥϝʔλҾ਺ʹͨ͠
    RSpec.shared_examples 'point increasing by day of the week' do |expected_point:|
    it "increase by #{expected_point}" do
    expect(point.point).to eq 0
    point.increase_by_day_of_the_week(wday)
    expect(point.point).to eq expected_point
    end
    end

    View Slide

  55. # shared_examples ΛࣙΊͨྫ
    RSpec.describe Point, type: :model do
    describe '#increase_by_day_of_the_week' do
    let(:point) { create :point, point: 0 }
    context 'on sunday' do
    let(:wday) { 0 }
    it "increase by 100" do
    expect(point.point).to eq 0
    point.increase_by_day_of_the_week(wday)
    expect(point.point).to eq 100
    end
    end
    context 'on monday' do
    let(:wday) { 1 }
    it "increase by 50" do
    expect(point.point).to eq 0
    point.increase_by_day_of_the_week(wday)
    expect(point.point).to eq 50
    end
    end
    # ུ
    end
    end

    View Slide

  56. it_behaves_like '○○'`
    ʮ○○ͷΑ͏ʹৼΔ෣͏ʯͰදݱͰ͖ͳ͍Α͏ͳέʔεͰ͸߇͑ͨ
    ΄͏͕Α͍ͷͰ͸

    View Slide

  57. ʮԶͳΒ͜͏ॻ͘ʯΈ͍ͨͳ࿩ืू͠
    ͍ͯ·͢

    View Slide

  58. ࠓ೔ͷ࿩͸࣍ͷGitHubϨϙδτϦʹ͍͍ͩͨ·
    ͱΊͯ·͢
    ࠓ೔࿩ͨ͠Ҏ֎ͷ߲໨΋͋Γ·͢
    https://github.com/willnet/rspec-style-guide

    View Slide

  59. ποίϛืू
    Issue, Pull Request͓·͍ͪͯ͠·͢

    View Slide

  60. Happy Testing!

    View Slide