├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── bin ├── rake ├── rspec └── setup ├── lib ├── dashboard.rb └── post.rb └── spec ├── dashboard_spec.rb ├── post_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | /test.db 3 | vendor 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.1 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 4.1" 4 | gem "database_cleaner" 5 | gem "factory_girl" 6 | gem "rspec", "~> 3.1" 7 | gem "rake" 8 | gem "sqlite3" 9 | gem "timecop" 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (4.1.8) 5 | activesupport (= 4.1.8) 6 | builder (~> 3.1) 7 | activerecord (4.1.8) 8 | activemodel (= 4.1.8) 9 | activesupport (= 4.1.8) 10 | arel (~> 5.0.0) 11 | activesupport (4.1.8) 12 | i18n (~> 0.6, >= 0.6.9) 13 | json (~> 1.7, >= 1.7.7) 14 | minitest (~> 5.1) 15 | thread_safe (~> 0.1) 16 | tzinfo (~> 1.1) 17 | arel (5.0.1.20140414130214) 18 | builder (3.2.2) 19 | database_cleaner (1.3.0) 20 | diff-lcs (1.2.5) 21 | factory_girl (4.5.0) 22 | activesupport (>= 3.0.0) 23 | i18n (0.6.11) 24 | json (1.8.1) 25 | minitest (5.4.3) 26 | rake (10.4.2) 27 | rspec (3.1.0) 28 | rspec-core (~> 3.1.0) 29 | rspec-expectations (~> 3.1.0) 30 | rspec-mocks (~> 3.1.0) 31 | rspec-core (3.1.7) 32 | rspec-support (~> 3.1.0) 33 | rspec-expectations (3.1.2) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.1.0) 36 | rspec-mocks (3.1.3) 37 | rspec-support (~> 3.1.0) 38 | rspec-support (3.1.2) 39 | sqlite3 (1.3.10) 40 | thread_safe (0.3.4) 41 | timecop (0.7.1) 42 | tzinfo (1.2.2) 43 | thread_safe (~> 0.1) 44 | 45 | PLATFORMS 46 | ruby 47 | 48 | DEPENDENCIES 49 | activerecord (~> 4.1) 50 | database_cleaner 51 | factory_girl 52 | rake 53 | rspec (~> 3.1) 54 | sqlite3 55 | timecop 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2015-2018 [thoughtbot, inc.](http://thoughtbot.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test Doubles / Simple Stubs 2 | 3 | Hey there! We're [thoughtbot](https://thoughtbot.com), a design and 4 | development consultancy that brings your digital product ideas to life. 5 | We also love to share what we learn. 6 | 7 | This coding exercise comes from [Upcase](https://thoughtbot.com/upcase), 8 | the online learning platform we run. It's part of the 9 | [Test Doubles](https://thoughtbot.com/upcase/test-doubles) course and is just one small sample of all 10 | the great material available on Upcase, so be sure to visit and check out the rest. 11 | 12 | ## Exercise Intro 13 | 14 | When testing one object, you may find that the other objects it interacts with get in the way. For example, when testing a controller, it can be cumbersome to ensure the model is used correctly: 15 | 16 | ``` ruby 17 | describe PostsController do 18 | describe "#index" do 19 | it "finds the 10 latest published posts" do 20 | 10.times { |i| create(:post, :published, title: "published#{i}") } 21 | create(:post, published: false, title: "unpublished") 22 | 23 | get :index 24 | 25 | expect(assigns[:posts].map(&:title)).to match_array(%w( 26 | published0 published1 published2 published3 published4 published5 27 | published6 published7 published8 published9 28 | )) 29 | end 30 | end 31 | end 32 | ``` 33 | 34 | In this example, the setup and verification phases become complex and hard to follow because we need to re-test the logic to find 10 published posts, even though this logic is already implemented and tested in the model layer. 35 | 36 | We can use stubs to clean it up: 37 | 38 | ``` ruby 39 | describe PostsController do 40 | describe "#index" do 41 | it "finds the 10 latest published posts" do 42 | posts = double("latest_published") 43 | allow(Post).to receive(:latest_published).and_return(posts) 44 | 45 | get :index 46 | 47 | expect(assigns[:posts]).to eq(posts) 48 | end 49 | end 50 | end 51 | ``` 52 | 53 | The `posts` variable above is called a "stub," which is a kind of "test double." 54 | 55 | You can create test doubles in RSpec by using the `double` method: 56 | 57 | ``` ruby 58 | posts = double("latest_published") 59 | ``` 60 | 61 | This creates an object that stands in for an actual list of published posts; it's like a stunt double for your actual object. Using the `double` method is very similar to calling `Object.new`, but tests that use doubles will fail with much clearer error messages. The `"latest_published"` string above is simply a name which will be used in test failure messages. 62 | 63 | You can stub out a method on any object (including a test double) by using `allow`: 64 | 65 | ``` ruby 66 | allow(Post).to receive(:latest_published).and_return(posts) 67 | ``` 68 | 69 | This changes the `Post` class to return `posts` whenever `latest_published` is called. Stubbed methods will revert to their regular behavior after each test runs. 70 | 71 | Using the `double` and `allow` methods, you can create simple stubs. 72 | 73 | In this exercise, you'll use simple stubs to clean up some tests. 74 | 75 | If you'd like to see a quick intro to test doubles, check out the related episode of [the Weekly Iteration](https://upcase.com/videos/stubs-mocks-spies-and-fakes). 76 | 77 | ## Instructions 78 | 79 | To start, you'll want to clone and run the setup script for the repo 80 | 81 | git clone git@github.com:thoughtbot-upcase-exercises/simple-stubs.git 82 | cd simple-stubs 83 | bin/setup 84 | 85 | After running `bin/setup`, edit `spec/dashboard_spec.rb` and look for test logic which is duplicated from `spec/post_spec.rb`. Use `double` and `allow` to create a stub for posts and eliminate this duplication. 86 | 87 | ## Tips and Tricks 88 | 89 | Useful links: 90 | 91 | - Check out our Weekly Iteration episode on [Stubs, Mocks, Spies, and Fakes](https://upcase.com/videos/stubs-mocks-spies-and-fakes) (specifically the first few minutes on `double` and method stubbing). 92 | - Check out the rspec-mocks guides to [test-doubles](https://github.com/rspec/rspec-mocks#test-doubles) and [method stubs](https://github.com/rspec/rspec-mocks#method-stubs) 93 | 94 | ## Featured Solution 95 | 96 | Check out the [featured solution branch](https://github.com/thoughtbot-upcase-exercises/simple-stubs/compare/featured-solution#toc) to 97 | see the approach we recommend for this exercise. 98 | 99 | ## Forum Discussion 100 | 101 | If you find yourself stuck, be sure to check out the associated 102 | [Upcase Forum discussion](https://forum.upcase.com/t/test-doubles-simple-stubs/4609) 103 | for this exercise to see what other folks have said. 104 | 105 | ## Next Steps 106 | 107 | When you've finished the exercise, head on back to the 108 | [Test Doubles](https://thoughtbot.com/upcase/test-doubles) course to find the next exercise, 109 | or explore any of the other great content on 110 | [Upcase](https://thoughtbot.com/upcase). 111 | 112 | ## License 113 | 114 | simple-stubs is Copyright © 2015-2018 thoughtbot, inc. It is free software, 115 | and may be redistributed under the terms specified in the 116 | [LICENSE](/LICENSE.md) file. 117 | 118 | ## Credits 119 | 120 | ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg) 121 | 122 | This exercise is maintained and funded by 123 | [thoughtbot, inc](http://thoughtbot.com/community). 124 | 125 | The names and logos for Upcase and thoughtbot are registered trademarks of 126 | thoughtbot, inc. 127 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rake' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rake', 'rake') 17 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Run this script immediately after cloning the codebase. 4 | # https://github.com/thoughtbot/guides/tree/master/protocol 5 | 6 | # Exit if any subcommand fails 7 | set -e 8 | 9 | # Set up Ruby dependencies via Bundler 10 | if ! command -v bundle > /dev/null; then 11 | gem install bundler --no-document --pre 12 | fi 13 | 14 | bundle install 15 | 16 | # Add bin to path if using thoughtbot dotfiles: 17 | # http://goo.gl/CPzari 18 | mkdir .git/safe 19 | -------------------------------------------------------------------------------- /lib/dashboard.rb: -------------------------------------------------------------------------------- 1 | require "post" 2 | 3 | class Dashboard 4 | def initialize(posts:) 5 | @posts = posts 6 | end 7 | 8 | def todays_posts 9 | @posts.today 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | validates :title, presence: true 3 | 4 | def self.today 5 | where( 6 | "posts.created_at >= ? AND posts.created_at <= ?", 7 | Time.now.beginning_of_day, 8 | Time.now.end_of_day 9 | ) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dashboard_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "dashboard" 3 | 4 | describe Dashboard do 5 | describe "#posts" do 6 | it "returns posts created today" do 7 | create :post, title: "first_today", created_at: Time.now.beginning_of_day 8 | create :post, title: "last_today", created_at: Time.now.end_of_day 9 | create :post, title: "yesterday", created_at: 1.day.ago.end_of_day 10 | dashboard = Dashboard.new(posts: Post.all) 11 | 12 | result = dashboard.todays_posts 13 | 14 | expect(result.map(&:title)).to match_array(%w(first_today last_today)) 15 | end 16 | end 17 | 18 | around do |example| 19 | Timecop.freeze { example.run } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/post_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "post" 3 | 4 | describe Post do 5 | describe "#today" do 6 | it "returns posts created today" do 7 | create :post, title: "first_today", created_at: Time.now.beginning_of_day 8 | create :post, title: "last_today", created_at: Time.now.end_of_day 9 | create :post, title: "yesterday", created_at: 1.day.ago.end_of_day 10 | 11 | result = Post.today 12 | 13 | expect(result.map(&:title)).to match_array(%w(first_today last_today)) 14 | end 15 | end 16 | 17 | around do |example| 18 | Timecop.freeze { example.run } 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rspec" 2 | require "active_record" 3 | require "database_cleaner" 4 | require "factory_girl" 5 | require "timecop" 6 | 7 | PROJECT_ROOT = File.expand_path("../..", __FILE__) 8 | 9 | $LOAD_PATH << PROJECT_ROOT 10 | 11 | ActiveRecord::Base.establish_connection( 12 | adapter: "sqlite3", 13 | database: File.join(PROJECT_ROOT, "test.db") 14 | ) 15 | 16 | class CreateSchema < ActiveRecord::Migration 17 | def self.up 18 | create_table :posts, force: true do |table| 19 | table.string :title 20 | table.timestamps 21 | end 22 | end 23 | end 24 | 25 | FactoryGirl.define do 26 | sequence(:title) { |n| "title#{n}text" } 27 | 28 | factory :post do 29 | title 30 | end 31 | end 32 | 33 | RSpec.configure do |config| 34 | config.include FactoryGirl::Syntax::Methods 35 | 36 | config.before(:suite) do 37 | CreateSchema.suppress_messages { CreateSchema.migrate(:up) } 38 | DatabaseCleaner.clean_with :deletion 39 | DatabaseCleaner.strategy = :transaction 40 | end 41 | 42 | config.before(:each) do 43 | DatabaseCleaner.start 44 | end 45 | 46 | config.after(:each) do 47 | DatabaseCleaner.clean 48 | end 49 | end 50 | --------------------------------------------------------------------------------