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

リーダブルテストコード / #vstat

リーダブルテストコード / #vstat

「リーダブルなテストコードについて考えよう ~VeriServe Test Automation Talk No.3~」で使用したスライドです。
https://veriserve-event.connpass.com/event/243280/

登壇動画はこちらで公開されています。
https://vimeo.com/742517199/e001ac43ac

<参考リンク>

Twitter https://twitter.com/jnchito

Blog https://blog.jnito.com/

Qiita https://qiita.com/jnchito

プロを目指す人のためのRuby入門[改訂2版]
https://gihyo.jp/book/2021/978-4-297-12437-3

Everyday Rails - RSpecによるRailsテスト入門
https://leanpub.com/everydayrailsrspec-jp/

https://twitter.com/jnchito/status/1552196880524013569

https://twitter.com/jnchito/status/1539100857991852032

https://twitter.com/jnchito/status/1539100861947064320

Clean Test Code Revised (by Shinichi Maeshima)
https://speakerdeck.com/willnet/clean-test-code-revised

【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?)
https://qiita.com/jnchito/items/2a5d3e15761fd413657a

なぜテストを書くの?(または書かないの?) 〜テストコードの7つの役割〜
https://speakerdeck.com/jnchito/number-tamarubykaigi01

テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~
https://qiita.com/jnchito/items/eb3cfa9f7db752dcb796

僕がRSpecでsubjectを使わない理由
https://blog.jnito.com/entry/2021/10/09/105651

Junichi Ito

July 27, 2022
Tweet

More Decks by Junichi Ito

Other Decks in Programming

Transcript

  1. Ϧʔμϒϧςετίʔυ
    גࣜձࣾιχοΫΨʔσϯɿҏ౻३Ұ
    2022-7-27 VeriServe Test Automation Talk No.3

    View full-size slide

  2. https://twitter.com/jnchito/status/1552196880524013569
    ͍͖ͳΓ༨ஊ
    ࠓ೔΄Μͱ͏ʹ͋ͬͨ࿩

    View full-size slide

  3. ͍͖ͳΓ༨ஊ
    φΠελΠϛϯάʂ!

    View full-size slide

  4. 5
    Hello, world! "
    • ҏ౻ ३Ұ
    • גࣜձࣾιχοΫΨʔσϯͷRailsϓϩάϥϚ
    • ϑΟϤϧυϒʔτΩϟϯϓͷϝϯλʔ
    • ฌݿݝ੢࿬ࢢࡏॅʢϦϞʔτϫʔΫྺ10೥ʣ
    • ςετίʔυྺ 15೥Ҏ্ʢJUnit → NUnit → RSpecʣ
    !KODIJUP
    CMPHKOJUPDPN

    View full-size slide

  5. 6
    Qiita
    • ϢʔβʔϥϯΩϯά1Ґʢ2022೥7݄ݱࡏʣ
    • QiitaදজϓϩάϥϜ །ҰͷDIAMONDड৆ऀ
    #

    View full-size slide

  6. 7
    ஶॻ
    ʮϓϩΛ໨ࢦ͢ਓͷͨΊͷRubyೖ໳ʯ
    ‣ ௨শɾνΣϦʔຊ$
    ‣ ग़൛ࣾɿٕज़ධ࿦ࣾ
    ‣ 2017೥11݄ ୈ̍൛ൃച
    ‣ 2021೥12݄ վగ̎൛ൃചʢRuby 3.0ʹରԠʣ
    3VCZͱ5%%͕ಉ࣌ʹֶ΂·͢ʂ
    ͦͯ͠ʜʜ
    ㊗ॏ൛ग़དྷʂʢࠓ೔࿈བྷ͕དྷ·ͨ͠&ʣ
    ͡Ύ͏͸Μ͠Ύ͍ͬͨ

    View full-size slide

  7. 8
    ༁ॻ
    ʮEveryday Rails - RSpecʹΑΔRailsςετೖ໳ʯ
    ‣ Aaron Sumner ஶ
    ‣ ग़൛ࣾɿLeanpub
    ‣ ిࢠॻ੶ͱͯ͠ൃചத
    ‣ 2014೥2݄ 1stϦϦʔε
    ‣ 2022೥1݄ Rails 7.0ʹରԠ
    34QFDͰ3BJMTΞϓϦΛςετ
    ͍ͨ͠ਓ͸ͪ͜ΒΛͲ͏ͧʂ

    View full-size slide

  8. 9
    ຊ೔͓࿩͢͠Δ͜ͱ
    • ςετίʔυʹ͓͍ͯɺա౓ͳDRY͸ಡΈ΍͢͞ͷఢ
    • ݡͯ͘ϩδΧϧͳςετίʔυΑΓɺ୭Ͱ΋ಡΊΔ۪௚ͳςετίʔυΛʂ
    • ೴಺ϝϞϦΛ࢖Θͳ͍ςετίʔυ΄ͲϦʔμϒϧ
    • ࣮ߦՄೳͳAPIυΩϡϝϯτͩͱࢥͬͯςετίʔυΛॻ͜͏
    • ϓϩάϥϚ͕ࣗ෼Ͱॻ͘Ϣχοτςετͷ࿩͕ϝΠϯͰ͢

    View full-size slide

  9. 10
    ඞཁͳࣄલ஌ࣝ
    • αϯϓϧίʔυ͸RSpecʢRubyʣͰॻ͖·͢
    • ͕ɺςετίʔυΛॻ͍ͨܦݧ͕͋Ε͹͍͍ͩͨཧղͰ͖Δ͸ͣ
    • ͦ͏ɺϦʔμϒϧͳςετίʔυͳΒͶʂ
    • จࣈ͕খ͍͞৔߹͸ɺ͖ͬ͞πΠʔτͨ͠εϥΠυΛ։͍͍ͯͩ͘͞
    if You.cannot.read_this?
    visit "https://speakerdeck.com/jnchito/readable-test-code-vstat"
    end

    View full-size slide

  10. ͱ͍͏Θ͚ͰɺͦΖͦΖຊฤ΁

    View full-size slide

  11. Ϧʔμϒϧςετίʔυ

    View full-size slide

  12. Q. ͜Μͳܦݧ͸͋Γ·ͤΜ͔ʁ

    View full-size slide

  13. https://twitter.com/jnchito/status/1539100857991852032

    View full-size slide

  14. Α͏Θ͔ΒΜͳΒ
    approveͪ͠ΌμϝͰ͢'

    View full-size slide

  15. https://twitter.com/jnchito/status/1539100861947064320
    ͖ͬ͞ͷπΠʔτͷଓ͖

    View full-size slide

  16. ςετίʔυʹ͓͍ͯɺա౓ͳ
    DRY͸ಡΈ΍͢͞ͷఢ
    DRY = Don't Repeat YourselfͷུͰίʔυ΍σʔλͷॏෳΛͳͤ͘ɺͱ͍͏ݪଇ

    View full-size slide

  17. ςετίʔυʹ͓͍ͯɺա౓ͳ
    DRY͸ಡΈ΍͢͞ͷఢ
    DRY = Don't Repeat YourselfͷུͰίʔυ΍σʔλͷॏෳΛͳͤ͘ɺͱ͍͏ݪଇ
    ੲͷ๻Ͱ͢
    ը૾͸ʮѱ॥؀ը૾δΣωϨʔλʯͰ࡞੒

    View full-size slide

  18. 19
    Ϧʔμϒϧͳςετίʔυͷ৚݅
    ✅ υΩϡϝϯτͷΑ͏ʹ্͔ΒԼʹૉ௚ʹಡΈԼͤΔ
    ✅ ม਺Λ࢖Θͣʹจࣈྻ΍਺஋Λϕλॻ͖͢Δ
    ✅ ڽͬͨςΫχοΫΛཚ༻͠ͳ͍
    ❌ ϧʔϓॲཧ΍৚݅෼ذ͕සൃ͢Δʢࢹઢ্͕Լͨ͠Γδϟϯϓͨ͠Γ͢Δʣ
    ❌ ਺஋΍จࣈྻͷΑ͏ͳ୯७ͳσʔλ·Ͱશ෦ม਺ʹ֨ೲ͞Ε͍ͯΔ
    ❌ shared examples΍subjectͳͲɺςεςΟϯάFWݻ༗ͷػೳΛཚ༻͍ͯ͠Δ
    ൓ରʹ͢ΔͱʮಡΈʹ͍͘ςετίʔυʯͷͰ͖͕͋Γʂʂ
    ϑϨʔϜϫʔΫ

    View full-size slide

  19. ݡͯ͘ϩδΧϧͳςετίʔυΑΓɺ
    ୭Ͱ΋ಡΊΔ۪௚ͳςετίʔυΛʂ
    ඇΤϯδχΞͰ΋ʂ

    View full-size slide

  20. ࣮ࡍʹૺ۰ͨ͠ςετίʔυͰઆ໌

    View full-size slide

  21. 22
    ࠓճςετ͢Δϝιου = UserΫϥεͷageϝιου
    def age
    today = Date.today
    this_years_birthday = Date.new(
    today.year, birth_date.month, birth_date.day)
    ret = today.year - birth_date.year
    if today < this_years_birthday
    ret -= 1
    end
    ret
    end
    • ࢓༷΍ϩδοΫ͸͋͑ͯઆ໌͠·ͤΜ
    ※ աڈʹ࣮ࡍʹίʔυϨϏϡʔͨ͠ϓϩάϥϜͱͦͷςετίʔυΛҰ෦վม͍ͯ͠·͢

    View full-size slide

  22. 23
    ๻͕ϨϏϡʔͨ͠ςετίʔυ͸͜Ε
    let(:user) { create(:user, birth_date: Faker::Date.birthday) }
    let(:this_year) { Date.today.year }
    let(:this_years_birthday) {
    Date.new(this_year, user.birth_date.month, user.birth_date.day) }
    let(:age) { this_year - user.birth_date.year }
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday)
    expect(user.age).to eq(age)
    end
    end
    context "஀ੜ೔Ҏલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday.yesterday)
    expect(user.age).to eq(age - 1)
    end
    end
    ໰୊ɿageϝιου͸ͲΜͳ࢓༷ʁ

    View full-size slide

  23. https://twitter.com/jnchito/status/1539100857991852032
    ࠶ܝ

    View full-size slide

  24. ͖ͬ͞ͷςετίʔυɺapprove͢Δʁ
    ✅:&4 ❌/0

    View full-size slide

  25. ๻͸͠·ͤΜ'
    ❌/0

    View full-size slide

  26. ͳͥͳΒϦʔμϒϧͰ͸ͳ͍͔Β
    ❌/0

    View full-size slide

  27. ςετίʔυͷ͔Β͘ΓΛಡΈղ͘
    ⚠ ղઆ͕ඞཁͳςετίʔυʹͳͬͯΔ࣌఺Ͱͦ΋ͦ΋͓͔͍͠Αʂ

    View full-size slide

  28. let(:user) { create(:user, birth_date: Faker::Date.birthday) }
    let(:this_year) { Date.today.year }
    let(:this_years_birthday) {
    Date.new(this_year, user.birth_date.month, user.birth_date.day) }
    let(:age) { this_year - user.birth_date.year }
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday)
    expect(user.age).to eq(age)
    end
    end
    context "஀ੜ೔Ҏલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday.yesterday)
    expect(user.age).to eq(age - 1)
    end
    end
    ςετ͢Δϝιου
    ςετ͢Δϝιου
    let(:x) { 1 } ɹ x = 1
    ͸ Έ͍ͨͳΠϝʔδ
    travel_to ͸Ҿ਺Ͱࢦఆͨ͠೔෇ʹγεςϜ೔෇Λมߋ͢Δϝιου

    View full-size slide

  29. let(:user) { create(:user, birth_date: Faker::Date.birthday) }
    let(:this_year) { Date.today.year }
    let(:this_years_birthday) {
    Date.new(this_year, user.birth_date.month, user.birth_date.day) }
    let(:age) { this_year - user.birth_date.year }
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday)
    expect(user.age).to eq(age)
    end
    end
    context "஀ੜ೔Ҏલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to(this_years_birthday.yesterday)
    expect(user.age).to eq(age - 1)
    end
    end
    travel_to ͸Ҿ਺Ͱࢦఆͨ͠೔෇ʹγεςϜ೔෇Λมߋ͢Δϝιου
    ςετ͢Δϝιου
    ςετ͢Δϝιου
    let(:x) { 1 } ɹ x = 1
    ͸ Έ͍ͨͳΠϝʔδ

    ←ϥϯμϜʹܾ·ΔUserͷੜ೥݄೔
    ←γεςϜ೔෇͔Βʮࠓ೥ʯΛऔಘʢ2022ͳͲʣ
    ←ಈతʹੜ੒͞ΕΔʮࠓ೥ͷ஀ੜ೔ʯ
    ←ಈతʹੜ੒͞ΕΔʮࠓ೥ͷ೥ྸʯ
    ্Ͱੜ੒ͨ͠ʮࠓ೥ͷ೥ྸʯͱageϝιουͷ໭Γ஋͕Ұக͢Δ͜ͱΛݕূ
    ←γεςϜ೔෇Λʮࠓ೥ͷ஀ੜ೔ͷલ೔ʯʹมߋ
    ্Ͱੜ੒ͨ͠ʮࠓ೥ͷ೥ྸʯΑΓ1ࡀए͘ͳΔ͜ͱΛݕূ
    ←γεςϜ೔෇Λʮࠓ೥ͷ஀ੜ೔ʯʹมߋ

    View full-size slide

  30. Θ͔Δ͜ͱɿ೴಺ϝϞϦΛফඅ͢Δ
    ςετίʔυ͸ϦʔμϒϧͰͳ͍
    ࢀߟจݙɿClean Test Code Revised (by Shinichi Maeshima)
    https://speakerdeck.com/willnet/clean-test-code-revised

    View full-size slide

  31. ͦ͜Ͱɺม਺Λͳͯ͘͠ΈΔ

    View full-size slide

  32. let(:user) { create(:user, birth_date: "1977/07/17".to_date) }
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to("2022/07/17".to_date)
    expect(user.age).to eq(45)
    end
    end
    context "஀ੜ೔ΑΓҎલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to("2022/07/16".to_date)
    expect(user.age).to eq(44)
    end
    end
    ςετ͢Δϝιου
    ςετ͢Δϝιου
    ্͔ΒԼʹಡΊΔΑ͏ʹͳͬͨ఺ʹ΋஫໨ʂ

    View full-size slide

  33. let(:user) { create(:user, birth_date: "1977/07/17".to_date) }
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to("2022/07/17".to_date)
    expect(user.age).to eq(45)
    end
    end
    context "஀ੜ೔ΑΓҎલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to("2022/07/16".to_date)
    expect(user.age).to eq(44)
    end
    end
    ςετ͢Δϝιου
    ςετ͢Δϝιου
    ্͔ΒԼʹಡΊΔΑ͏ʹͳͬͨ఺ʹ΋஫໨ʂ
    ←ੜ೥݄೔͸1977೥7݄17೔
    ←γεςϜ೔෇Λ2022೥7݄17೔ͱ͢Δ
    ageϝιου͸ʮ45ࡀʯΛฦ͢
    ←γεςϜ೔෇Λ2022೥7݄16೔ͱ͢Δ
    ageϝιου͸ʮ44ࡀʯΛฦ͢
    ্͔ΒԼʹಡΊΔΑ͏ʹͳͬͨ఺ʹ΋஫໨ʂ

    View full-size slide

  34. ೴಺ϝϞϦͷফඅΛ཈͑Δ΄Ͳ
    ϦʔμϒϧͳςετίʔυʹͳΔ
    ࢀߟจݙɿClean Test Code Revised (by Shinichi Maeshima)
    https://speakerdeck.com/willnet/clean-test-code-revised

    View full-size slide

  35. 38
    ςετίʔυ͸Ұछͷ࢓༷ॻ
    def age
    today = Date.today
    this_years_birthday = Date.new(
    today.year, birth_date.month, birth_date.day)
    ret = today.year - birth_date.year
    if today < this_years_birthday
    ret -= 1
    end
    ret
    end
    ͋
    ͋
    • ςετίʔυΛݟΕ͹ϝιουͷৼΔ෣͍͕͙͢Θ͔Δͷ͕ཧ૝
    context "஀ੜ೔Ҏ߱ͷ৔߹" do
    it "஀ੜ೔Λա͗ͨ೥ྸ͕ฦΔ" do
    travel_to("2022/07/17".to_date)
    expect(user.age).to eq(45)
    end
    end
    context "஀ੜ೔ΑΓҎલͷ৔߹" do
    it "஀ੜ೔લͷ೥ྸ͕ฦΔ" do
    travel_to("2022/07/16".to_date)
    expect(user.age).to eq(44)
    end
    end
    RubyΛ஌Βͳ͍ਓͰ΋ςετίʔυ
    ͳΒΘ͔Δɺͱ͍͏ঢ়ଶΛ໨ࢦͦ͏
    #

    View full-size slide

  36. 39
    APIυΩϡϝϯτͷαϯϓϧίʔυͱߟ͑ํ͸ಉ͡
    APIυΩϡϝϯτ΋αϯϓϧίʔυ͸ϕλॻ͖͕جຊ
    ग़య: https://docs.ruby-lang.org/ja/latest/method/File/s/basename.html

    View full-size slide

  37. 40
    ΋͠΋APIυΩϡϝϯτ͕ϕλॻ͖͡Όͳ͔ͬͨΒ……
    ,

    View full-size slide

  38. ࣮ߦՄೳͳAPIυΩϡϝϯτͩͱ
    ࢥͬͯςετίʔυΛॻ͜͏
    ςετίʔυ͸ϓϩάϥϜ
    ͡Όͳͯ͘υΩϡϝϯτʂ

    View full-size slide

  39. ͦͷଞͷτϐοΫ

    View full-size slide

  40. 43
    E2EςετͰ΋ߟ͑ํ͸ಉ͡
    click_link user.name
    expect(page).to have_text "#{user.name}ͷ೔ه"
    ❌ ը໘্ͷཁૉΛม਺Ͱࢦఆ͢Δ
    click_link "͋Γ͢"
    expect(page).to have_text "͋Γ͢ͷ೔ه"
    ✅ ը໘্ͷཁૉΛϕλॻ͖Ͱࢦఆ͢Δ
    ม਺Λଟ༻͢ΔͱνϦπϞͰ͡Θ͡Θͱ೴಺ϝϞϦΛফඅ͠·͢……ʂʂ
    ※ E2Eςετ = End to Endςετɻϒϥ΢βૢ࡞ΛࣗಈԽ͠ɺγεςϜશମͷಈ࡞Λݕূ͢Δ

    View full-size slide

  41. 44
    ·ͩ͋ΔϦʔμϒϧʹ͢Δίπ
    • ࣮ࡍͷϢʔεέʔεʹ͍ۙςετσʔλΛ࢖༻͢Δ
    ‣ ʮ͋͋͋ʯʮςετςετʯʮϢʔβʔ̍ʯΈ͍ͨͳςετσʔλ͸NG
    • describe/context/itͷઆ໌Λஸೡʹॻ͘
    ‣ ʮit "ద੾ͳ஋Λฦ͢"ʯΈ͍ͨͳ۩ମੑͷͳ͍આ໌͸NG
    • ͢΂ͯͷ৘ใ͕̍ը໘ʹऩ·Δςετίʔυ͕ཧ૝
    ‣ ্ԼεΫϩʔϧ͕සൟʹൃੜͨ͠ΓɺଞͷϑΝΠϧΛݟʹߦ͔ͳ͖Ό͍͚ͳ͍ͷ͸NG
    • DRYېࢭ͸͋͘·ͰݪଇɻదٓϝϦοτͱσϝϦοτΛఱṝʹ͔͚Δ
    ‣ ʮ໌֬ͳϝϦοτ͕͋ΔDRYʯ΍ʮՄಡੑΛଛͳΘͳ͍ந৅Խʯ·Ͱ์غ͢Δͷ͸NG

    View full-size slide

  42. 45
    ৄ͘͠͸WebͰʂ
    • ʲॳ৺ऀ޲͚ʳςετίʔυͷํ਑Λߟ͑ΔʢԿΛςετ͢΂͖͔ʁͲΜͳςετΛॻ͘΂͖͔ʁʣ
    https://qiita.com/jnchito/items/2a5d3e15761fd413657a
    • ͳͥςετΛॻ͘ͷʁʢ·ͨ͸ॻ͔ͳ͍ͷʁʣ ʙςετίʔυͷ7ͭͷ໾ׂʙ
    https://speakerdeck.com/jnchito/number-tamarubykaigi01
    • ςετίʔυͷظ଴஋͸DRYΛࣺͯͯϕλॻ͖͢Δ ʙςετίʔυͷॏཁͳ໾ׂͱ͸ʁʙ
    https://qiita.com/jnchito/items/eb3cfa9f7db752dcb796
    • ๻͕RSpecͰsubjectΛ࢖Θͳ͍ཧ༝
    https://blog.jnito.com/entry/2021/10/09/105651

    View full-size slide

  43. 47
    ࠓ೔ͷ͓͞Β͍
    subject { "ςετίʔυ" }
    • subject ʹ͓͍ͯɺա౓ͳDRY͸ಡΈ΍͢͞ͷఢ
    • ݡͯ͘ϩδΧϧͳ subject ΑΓɺ୭Ͱ΋ಡΊΔ۪௚ͳ subject Λʂ
    • ೴಺ϝϞϦΛ࢖Θͳ͍ subject ΄ͲϦʔμϒϧ
    • ࣮ߦՄೳͳAPIυΩϡϝϯτͩͱࢥͬͯ subject Λॻ͜͏
    ͋ΕʁͳΜ͔ಡΈͮΒ͍ͳ……
    subject { "foo" } ɹ subject = "foo"
    ※ɹɹɹɹɹɹɹɹɹɹɹ ͸ ɹɹɹɹɹɹ Έ͍ͨͳΠϝʔδ

    View full-size slide

  44. ͓ͬͱɺ͍͢·ͤΜʂ
    ͍ͭɺDRYʹॻ͖ͨ͘ͳΔΫη͕-

    View full-size slide

  45. ͪΌΜͱϕλॻ͖͠·͢.

    View full-size slide

  46. 50
    ࠓ೔ͷ͓͞Β͍
    • ςετίʔυʹ͓͍ͯɺա౓ͳDRY͸ಡΈ΍͢͞ͷఢ
    • ݡͯ͘ϩδΧϧͳςετίʔυΑΓɺ୭Ͱ΋ಡΊΔ۪௚ͳςετίʔυΛʂ
    • ೴಺ϝϞϦΛ࢖Θͳ͍ςετίʔυ΄ͲϦʔμϒϧ
    • ࣮ߦՄೳͳAPIυΩϡϝϯτͩͱࢥͬͯςετίʔυΛॻ͜͏

    View full-size slide

  47. https://twitter.com/jnchito/status/1539100857991852032
    ࠶ܝʢ2ճ໨ʣ

    View full-size slide

  48. ϦʔμϒϧͳςετίʔυΛॻ͍ͯ
    ࣗ৴Λ΋ͬͯϤγʂͯ͠΋Β͍·͠ΐ͏
    -(5.
    jnchito

    View full-size slide

  49. ͝ਗ਼ௌ
    !KODIJUP
    CMPHKOJUPDPN
    ɹɹɹ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View full-size slide