├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── lib ├── 10_drink.rb ├── 11a_pet.rb ├── 11b_cat.rb ├── 11c_dog.rb ├── 12_magic_seven.rb ├── 13_input_output.rb ├── 14_find_number.rb ├── 15_binary_main.rb ├── 15a_binary_game.rb ├── 15b_binary_search.rb ├── 15c_random_number.rb ├── 16_caesar_main.rb ├── 16a_caesar_breaker.rb ├── 16b_caesar_translator.rb └── 16c_database.rb ├── spec ├── 01_string_spec.rb ├── 02_array_spec.rb ├── 03_number_spec.rb ├── 04_hash_spec.rb ├── 05_truthy_falsy_spec.rb ├── 06_equality_spec.rb ├── 07_all_contain_exactly_spec.rb ├── 08_change_spec.rb ├── 09_custom_matcher_spec.rb ├── 10_drink_spec.rb ├── 11a_shared_example_spec.rb ├── 11b_cat_spec.rb ├── 11c_dog_spec.rb ├── 12_magic_seven_spec.rb ├── 13_input_output_spec.rb ├── 14_find_number_spec.rb ├── 15a_binary_game_spec.rb ├── 15b_binary_search_spec.rb ├── 16a_caesar_breaker_spec.rb ├── 16b_caesar_translator_spec.rb └── spec_helper.rb └── spec_answers ├── 01_string_answer.rb ├── 02_array_answer.rb ├── 03_number_answer.rb ├── 04_hash_answer.rb ├── 06_equality_answer.rb ├── 07_all_contain_exactly_answer.rb ├── 08_change_answer.rb ├── 09_custom_matcher_answer.rb ├── 10_drink_answer.rb ├── 11b_cat_answer.rb ├── 11c_dog_answer.rb ├── 12_magic_seven_answer.rb ├── 13_input_output_answer.rb ├── 14_find_number_answer.rb ├── 15a_binary_game_answer.rb └── 16a_caesar_answer.rb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve something that is not working correctly 4 | title: "Bug - :" 5 | labels: "Status: Needs Review, Type: Bug" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | Complete the following REQUIRED checkboxes: 12 | - [ ] I have thoroughly read and understand [The Odin Project Contributing Guide](https://github.com/TheOdinProject/.github/blob/main/CONTRIBUTING.md) 13 | - [ ] The title of this issue follows the `Bug - location of bug: brief description of bug` format, e.g. `Bug - Exercises: File type incorrect for all test files` 14 | 15 | The following checkbox is OPTIONAL: 16 | 17 | - [ ] I would like to be assigned this issue to work on it 18 | 19 |
20 | 21 | **1. Description of the Bug:** 22 | 23 | 24 | 25 | **2. How To Reproduce:** 26 | 33 | 34 | 35 | **3. Expected Behavior:** 36 | 43 | 44 | 45 | **4. Desktop/Device:** 46 | 47 | - Device: 48 | - OS: 49 | - Browser: 50 | - Version: 51 | 52 | **5. Additional Information:** 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature or enhancement for this project 4 | title: "" 5 | labels: "Status: Needs Review" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | Complete the following REQUIRED checkboxes: 12 | - [ ] I have thoroughly read and understand [The Odin Project Contributing Guide](https://github.com/TheOdinProject/.github/blob/main/CONTRIBUTING.md) 13 | - [ ] The title of this issue follows the `location for request: brief description of request` format, e.g. `Exercises: Add exercise on XYZ` 14 | 15 | The following checkbox is OPTIONAL: 16 | 17 | - [ ] I would like to be assigned this issue to work on it 18 | 19 |
20 | 21 | **1. Description of the Feature Request:** 22 | 25 | 26 | 27 | **2. Acceptance Criteria:** 28 | 34 | 35 | 36 | **3. Additional Information:** 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Because 4 | 5 | 6 | 7 | ## This PR 8 | 9 | 10 | 11 | ## Issue 12 | 19 | Closes #XXXXX 20 | 21 | ## Additional Information 22 | 23 | 24 | 25 | ## Pull Request Requirements 26 | 27 | - [ ] I have thoroughly read and understand [The Odin Project Contributing Guide](https://github.com/TheOdinProject/.github/blob/main/CONTRIBUTING.md) 28 | - [ ] The title of this PR follows the `location of change: brief description of change` format, e.g. `String spec: Update instructions for clarity` 29 | - [ ] The `Because` section summarizes the reason for this PR 30 | - [ ] The `This PR` section has a bullet point list describing the changes in this PR 31 | - [ ] If this PR addresses an open issue, it is linked in the `Issue` section 32 | - [ ] If this PR includes changes in the `spec` folder, they are also updated in the corresponding file in the `spec_answers` folder (with passing tests). 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'Gemfile' 4 | - 'spec/spec_helper.rb' 5 | NewCops: enable 6 | Metrics/BlockLength: 7 | Exclude: 8 | - 'spec/*' 9 | - 'spec_answers/*' 10 | Layout/LineLength: 11 | Exclude: 12 | - 'spec/*' 13 | - 'spec_answers/*' 14 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # gem "rails" 8 | gem 'rspec-core', '3.9.2' 9 | gem 'rspec-expectations', '3.9.2' 10 | gem 'rspec-mocks', '3.9.1' 11 | gem 'rspec-support', '3.9.3' 12 | gem 'rubocop', '~> 1.60' 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | diff-lcs (1.5.0) 6 | json (2.7.1) 7 | language_server-protocol (3.17.0.3) 8 | parallel (1.24.0) 9 | parser (3.3.0.5) 10 | ast (~> 2.4.1) 11 | racc 12 | racc (1.7.3) 13 | rainbow (3.1.1) 14 | regexp_parser (2.9.0) 15 | rexml (3.2.6) 16 | rspec-core (3.9.2) 17 | rspec-support (~> 3.9.3) 18 | rspec-expectations (3.9.2) 19 | diff-lcs (>= 1.2.0, < 2.0) 20 | rspec-support (~> 3.9.0) 21 | rspec-mocks (3.9.1) 22 | diff-lcs (>= 1.2.0, < 2.0) 23 | rspec-support (~> 3.9.0) 24 | rspec-support (3.9.3) 25 | rubocop (1.60.2) 26 | json (~> 2.3) 27 | language_server-protocol (>= 3.17.0) 28 | parallel (~> 1.10) 29 | parser (>= 3.3.0.2) 30 | rainbow (>= 2.2.2, < 4.0) 31 | regexp_parser (>= 1.8, < 3.0) 32 | rexml (>= 3.2.5, < 4.0) 33 | rubocop-ast (>= 1.30.0, < 2.0) 34 | ruby-progressbar (~> 1.7) 35 | unicode-display_width (>= 2.4.0, < 3.0) 36 | rubocop-ast (1.30.0) 37 | parser (>= 3.2.1.0) 38 | ruby-progressbar (1.13.0) 39 | unicode-display_width (2.5.0) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | rspec-core (= 3.9.2) 46 | rspec-expectations (= 3.9.2) 47 | rspec-mocks (= 3.9.1) 48 | rspec-support (= 3.9.3) 49 | rubocop (~> 1.60) 50 | 51 | BUNDLED WITH 52 | 2.3.14 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Odin Project 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 | # RSpec Playground 2 | The purpose of this repository is to provide 'hands-on' RSpec lessons. These lessons were designed to equip students of the [The Odin Project](https://www.theodinproject.com/) to write tests for their Tic-Tac-Toe and to TDD their Connect Four. 3 | 4 | ## Topics 5 | These lessons cover many topics, but it does not cover everything that RSpec is capable of testing. You should expect to learn foundational knowledge on the following topics: 6 | - Let Variables 7 | - Implicit and Explicit Subject 8 | - A variety of built-in-matchers 9 | - Setting values for instance variables to create test conditions 10 | - The 'Arrange, Act & Assert' testing pattern 11 | - Stubs, Mocks, and Doubles 12 | - Test Driven Development 13 | 14 | ## Set-Up 15 | Run `rbenv versions` to confirm that you have the same Ruby version installed as the one indicated in the `.ruby-version` file. If you do not have this version installed, please refer back to these [instructions](https://www.theodinproject.com/lessons/ruby-installing-ruby) to install it. 16 | 17 | Run `bundle install` from the root directory. 18 | 19 | ## How to use this playground 20 | These lessons are numbered 01 - 16, in the **spec** folder. Start with the file: spec/01_string_spec.rb. The first 9 lessons are self-contained in the spec file. Starting with lesson 10, there will be 1-3 corresponding files in the lib folder. 21 | 22 | If you get stuck on a lesson, there is a corresponding answer file located in the spec_answer folder. 23 | 24 | ## Running Tests 25 | Since this repository is full of tests, it is recommended to only run rspec on one individual file at at time. For example, to run the first test file from the root of this directory: 26 | 27 |
rspec spec/01_string_spec.rb
28 | 29 | Tip: If you have tab completion set-up, you can hit 'tab' after the first few characters of the file name. 30 | -------------------------------------------------------------------------------- /lib/10_drink.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # very basic Drink class 4 | class Drink 5 | attr_reader :type, :ounces 6 | 7 | def initialize(type = 'water', ounces = 16) 8 | @type = type 9 | @ounces = ounces 10 | end 11 | 12 | def full? 13 | ounces >= 16 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/11a_pet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # base class for a pet 4 | class Pet 5 | attr_reader :name, :breed, :color 6 | 7 | def initialize(name, breed = nil, color = nil) 8 | @name = name 9 | @breed = breed 10 | @color = color 11 | end 12 | 13 | def meal_time 14 | "#{name} paws at empty food bowl" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/11b_cat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11a_pet' 4 | 5 | # Cat is a sub-class of Pet 6 | class Cat < Pet 7 | def talk 8 | 'meow' 9 | end 10 | 11 | def hiding? 12 | true 13 | end 14 | 15 | def hungry? 16 | false 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/11c_dog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11a_pet' 4 | 5 | # Dog is a sub-class of Pet 6 | class Dog < Pet 7 | def talk 8 | 'WOOF!' 9 | end 10 | 11 | def sleeping? 12 | true 13 | end 14 | 15 | def barking? 16 | false 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/12_magic_seven.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Given any number, the final number will always be 7 4 | class MagicSeven 5 | attr_reader :random_number 6 | 7 | def initialize(random_number = rand(0..20)) 8 | @random_number = random_number 9 | end 10 | 11 | def add_nine(number) 12 | number + 9 13 | end 14 | 15 | def multiply_by_two(number) 16 | number * 2 17 | end 18 | 19 | def subtract_four(number) 20 | number - 4 21 | end 22 | 23 | def divide_by_two(number) 24 | number / 2 25 | end 26 | 27 | def subtract_random_number(number) 28 | number - random_number 29 | end 30 | 31 | def play 32 | step_one = add_nine(random_number) 33 | step_two = multiply_by_two(step_one) 34 | step_three = subtract_four(step_two) 35 | step_four = divide_by_two(step_three) 36 | subtract_random_number(step_four) 37 | end 38 | 39 | # This method is not used in testing. Its purpose is to show how the game 40 | # works. Run it in the console to see how, by uncommenting the appropriate 41 | # commands at the bottom of this file. 42 | 43 | def display_result 44 | step_one = add_nine(random_number) 45 | step_two = multiply_by_two(step_one) 46 | step_three = subtract_four(step_two) 47 | step_four = divide_by_two(step_three) 48 | puts <<~HEREDOC 49 | 50 | MAGIC SEVEN 51 | 52 | Magic Seven can take any random number and turn that number into the 53 | number 7, using the same five mathematical operations every time! 54 | 55 | For example, let's start with the random number #{random_number}. 56 | 57 | 1. Add nine: 58 | #{random_number} + 9 = #{step_one} 59 | 60 | 2. Take #1 answer and multiply by two: 61 | #{step_one} * 2 = #{step_two} 62 | 63 | 3. Take #2 answer and subtract four: 64 | #{step_two} - 4 = #{step_three} 65 | 66 | 4. Take #3 answer and divide by two: 67 | #{step_three} / 2 = #{step_four} 68 | 69 | 5. Take #4 answer and subtract the original random number: 70 | #{step_four} - #{random_number} = #{play} 71 | 72 | Step #5 will always be 7! 73 | 74 | HEREDOC 75 | end 76 | end 77 | 78 | # game = MagicSeven.new 79 | # game.play 80 | # game.display_result 81 | -------------------------------------------------------------------------------- /lib/13_input_output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file can be run in the console by uncommenting the appropriate 4 | # commands at the bottom of the file. Be sure to comment them out before 5 | # running rspec to avoid errors. 6 | 7 | # Game that is nearly impossible to test. 8 | # Rewritten below as the easy-to-test NumberGame class with isolated methods. 9 | class ImpossibleToTestGame 10 | attr_reader :solution, :count, :guess 11 | 12 | def initialize(solution = rand(0..9), guess = nil, count = 0) 13 | @solution = solution 14 | @guess = guess 15 | @count = count 16 | end 17 | 18 | def play_game 19 | puts "Let's play a game called 'Guess a random number!'" 20 | loop do 21 | player_input 22 | @count += 1 23 | break if @guess == solution.to_s 24 | end 25 | final_message 26 | end 27 | 28 | def player_input 29 | loop do 30 | puts 'Choose a digit between 0 and 9' 31 | @guess = gets.chomp 32 | break if @guess.match(/^[0-9]$/) 33 | end 34 | end 35 | 36 | def final_message 37 | if @count == 1 38 | puts 'LUCKY GUESS!' 39 | elsif @count < 4 40 | puts "Congratulations! You picked the random number in #{@count} guesses!" 41 | else 42 | puts "That was hard. It took you #{@count} guesses!" 43 | end 44 | end 45 | end 46 | 47 | # NumberGame is the exact same game as the above ImpossibleToTestGame. 48 | # NumberGame has small, isolated methods that are easy to test. 49 | class NumberGame 50 | attr_reader :solution, :count, :guess 51 | 52 | def initialize(solution = rand(0..9), guess = nil, count = 0) 53 | @solution = solution 54 | @guess = guess 55 | @count = count 56 | end 57 | 58 | def play_game 59 | puts "Let's play a game called 'Guess a random number!'" 60 | turn_order until game_over? 61 | final_message 62 | end 63 | 64 | def turn_order 65 | player_turn 66 | @count += 1 67 | end 68 | 69 | def player_turn 70 | loop do 71 | @guess = verify_input(player_input) 72 | break if @guess 73 | 74 | puts 'Input error!' 75 | end 76 | end 77 | 78 | def verify_input(number) 79 | return number if number.match?(/^[0-9]$/) 80 | end 81 | 82 | def game_over? 83 | guess == solution.to_s 84 | end 85 | 86 | def final_message 87 | if @count == 1 88 | puts 'LUCKY GUESS!' 89 | elsif @count < 4 90 | puts "Congratulations! You picked the random number in #{@count} guesses!" 91 | else 92 | puts "That was hard. It took you #{@count} guesses!" 93 | end 94 | end 95 | 96 | private 97 | 98 | def player_input 99 | puts 'Choose a digit between 0 and 9' 100 | gets.chomp 101 | end 102 | end 103 | 104 | # game = NumberGame.new 105 | # game.play_game 106 | 107 | # game = ImpossibleToTestGame.new 108 | # game.play_game 109 | -------------------------------------------------------------------------------- /lib/14_find_number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # class for computer to find random number 4 | class FindNumber 5 | attr_reader :min, :max, :answer, :guess 6 | 7 | def initialize(min, max, answer = RandomNumber.new(min, max), guess = nil) 8 | @min = min 9 | @max = max 10 | # The RandomNumber class will have an instance variable '@value' 11 | @answer = answer.value 12 | @guess = guess 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/15_binary_main.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/15a_binary_game' 4 | require_relative '../lib/15b_binary_search' 5 | require_relative '../lib/15c_random_number' 6 | 7 | # This file can be run in the console. 8 | 9 | minimum = 1 10 | maximum = 100 11 | game = BinaryGame.new(minimum, maximum) 12 | game.play_game 13 | -------------------------------------------------------------------------------- /lib/15a_binary_game.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop: disable Layout/LineLength 4 | 5 | # require_relative '../lib/15c_random_number' 6 | # require_relative '../lib/15b_binary_search' 7 | 8 | # class for computer to find random number 9 | class BinaryGame 10 | def initialize(minimum, maximum, random_number = RandomNumber.new(minimum, maximum)) 11 | @minimum = minimum 12 | @maximum = maximum 13 | @random_number = random_number 14 | @guess_count = 0 15 | end 16 | 17 | def play_game 18 | introduction 19 | mode = player_input(1, 2) 20 | update_random_number if mode == 1 21 | puts "\nUsing a binary search, any number can be found in #{maximum_guesses} guesses or less!\n\n" 22 | binary_search = create_binary_search 23 | display_binary_search(binary_search) 24 | puts "As predicted, the computer found it in #{@guess_count} guesses." 25 | end 26 | 27 | def player_input(min, max) 28 | loop do 29 | user_input = gets.chomp 30 | verified_number = verify_input(min, max, user_input.to_i) if user_input.match?(/^\d+$/) 31 | return verified_number if verified_number 32 | 33 | puts "Input error! Please enter a number between #{min} or #{max}." 34 | end 35 | end 36 | 37 | def verify_input(min, max, input) 38 | return input if input.between?(min, max) 39 | end 40 | 41 | def update_random_number 42 | puts "Enter a number between #{@minimum} and #{@maximum}" 43 | number_input = player_input(@minimum, @maximum) 44 | @random_number.update_value(number_input) 45 | end 46 | 47 | def maximum_guesses 48 | (Math.log2(@maximum - @minimum) + 1).to_i 49 | end 50 | 51 | def create_binary_search 52 | BinarySearch.new(@minimum, @maximum, @random_number) 53 | end 54 | 55 | def display_binary_search(binary_search) 56 | display_turn_order(binary_search) until binary_search.game_over? 57 | end 58 | 59 | def display_turn_order(binary_search) 60 | binary_search.make_guess 61 | @guess_count += 1 62 | display_guess(binary_search) 63 | binary_search.update_range 64 | end 65 | 66 | private 67 | 68 | def introduction 69 | puts <<~HEREDOC 70 | 71 | \e[32mWatch the computer find a number between #{@minimum} and #{@maximum} using a binary search.\e[0m 72 | 73 | The computer-generated random number is \e[32m#{@random_number.value}\e[0m. 74 | Would you like to choose your own number? 75 | 76 | \e[32m[1]\e[0m Choose a new number 77 | \e[32m[2]\e[0m Keep the randomly-generated number 78 | 79 | HEREDOC 80 | end 81 | 82 | def display_guess(binary_search) 83 | range = (@minimum..@maximum).to_a 84 | sleep(2) 85 | puts 86 | range.each do |number| 87 | print_number(binary_search.min, binary_search.max, number) 88 | end 89 | puts "\nGuess ##{@guess_count} -> \e[32m#{binary_search.guess}\e[0m\n\n" 90 | end 91 | 92 | def print_number(min, max, number) 93 | if number == (min + max) / 2 94 | print "\e[32m#{number} \e[0m" 95 | elsif number.between?(min, max) 96 | print "#{number} " 97 | else 98 | print "\e[91m#{number} \e[0m" 99 | end 100 | end 101 | end 102 | # rubocop: enable Layout/LineLength 103 | -------------------------------------------------------------------------------- /lib/15b_binary_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # class for computer to find the answer using a binary search 4 | class BinarySearch 5 | attr_reader :min, :max, :answer, :guess 6 | 7 | def initialize(min, max, answer = RandomNumber.new(min, max), guess = nil) 8 | @min = min 9 | @max = max 10 | @answer = answer.value 11 | @guess = guess 12 | end 13 | 14 | def make_guess 15 | @guess = (min + max) / 2 16 | end 17 | 18 | def game_over? 19 | guess == answer 20 | end 21 | 22 | def update_range 23 | guess < answer ? @min = guess + 1 : @max = guess - 1 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/15c_random_number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # class to generate random number 4 | class RandomNumber 5 | attr_reader :value 6 | 7 | def initialize(minimum, maximum) 8 | @value = rand(minimum..maximum) 9 | end 10 | 11 | def update_value(number) 12 | @value = number 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/16_caesar_main.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/16a_caesar_breaker' 4 | require_relative '../lib/16b_caesar_translator' 5 | require_relative '../lib/16c_database' 6 | 7 | # This file can be run in the console. 8 | 9 | phrase = CaesarBreaker.new('Rovvy, Gybvn!') 10 | phrase.decrypt 11 | -------------------------------------------------------------------------------- /lib/16a_caesar_breaker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | require_relative '../lib/16c_database' 4 | 5 | # breaks the Caesar Cipher code 6 | class CaesarBreaker 7 | include Database 8 | 9 | def initialize(message, translator = CaesarTranslator.new(message)) 10 | @message = message 11 | @translator = translator 12 | @decrypted_messages = [] 13 | end 14 | 15 | def decrypt 16 | create_decrypted_messages 17 | save_decrypted_messages 18 | end 19 | 20 | def create_decrypted_messages 21 | 26.times do |shift| 22 | @decrypted_messages << @translator.translate(shift) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/16b_caesar_translator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | # translates a Caesar Cipher code 4 | class CaesarTranslator 5 | attr_reader :message 6 | 7 | def initialize(message) 8 | @message = message 9 | end 10 | 11 | def translate(shift, result = '') 12 | message.each_char do |char| 13 | base = char.ord < 91 ? 65 : 97 14 | result << character_shift(char, base, shift) 15 | end 16 | result 17 | end 18 | 19 | private 20 | 21 | def character_shift(char, base, shift) 22 | char_num = char.ord 23 | if char_num.between?(65, 90) || char_num.between?(97, 122) 24 | rotation = (((char_num - base) + shift) % 26) + base 25 | rotation.chr 26 | else 27 | char 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/16c_database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | require 'yaml' 4 | 5 | # Contains methods to save or load a game 6 | module Database 7 | def save_decrypted_messages 8 | Dir.mkdir '16_cipher' unless Dir.exist? '16_cipher' 9 | filename = "#{create_filename}.yaml" 10 | File.open("16_cipher/#{filename}", 'w') { |file| file.write save_to_yaml } 11 | display_file_location(filename) 12 | rescue SystemCallError => e 13 | puts "Error while writing to file #{filename}." 14 | puts e 15 | end 16 | 17 | def save_to_yaml 18 | YAML.dump( 19 | 'message' => @message, 20 | 'decrypted_messages' => @decrypted_messages 21 | ) 22 | end 23 | 24 | private 25 | 26 | def create_filename 27 | @message.scan(/\w+/).join 28 | end 29 | 30 | def display_file_location(file_name) 31 | puts "'#{@message}' has been decoded." 32 | puts "The 25 possibilities are saved in a file in 16_cipher/#{file_name}" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/01_string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe String do 4 | # Let creates a helper method with a memoized value that is cached for the 5 | # same example but not across different examples. Let is lazy-evaluated; 6 | # it is not evaluated until the first time the method it defines is invoked. 7 | # https://medium.com/@tomkadwill/all-about-rspec-let-a3b642e08d39 8 | 9 | let(:favorite_color) { String.new('blue') } 10 | 11 | # Use a context block to make your tests clear and well organized. 12 | # It is not required, but it is generally used to explain any conditionals. 13 | # Here are some examples of words you should start your context block with: 14 | # if, when, unless, with, without, for, before, after, during 15 | 16 | context 'when a let variable is used' do 17 | it 'is the value of assigned let variable' do 18 | expect(favorite_color).to eq('blue') 19 | end 20 | end 21 | 22 | context 'when the let variable is overridden' do 23 | let(:favorite_color) { String.new('green') } 24 | 25 | it 'is the updated value of the let variable' do 26 | expect(favorite_color).to eq('green') 27 | end 28 | end 29 | 30 | # Let variables reset between examples. 31 | context 'when the overridden value is out of scope' do 32 | it 'is the value of original let variable' do 33 | expect(favorite_color).to eq('blue') 34 | end 35 | end 36 | end 37 | 38 | # ASSIGNMENT 39 | 40 | describe String do 41 | # Create a let variable that will pass the first test. 42 | 43 | # remove the 'x' before running this test 44 | xit 'is equal to tacos' do 45 | expect(favorite_food).to eq('tacos') 46 | end 47 | 48 | # remove the 'x' before running this test 49 | context 'when favorite food is updated' do 50 | # Change the favorite_food let variable. 51 | 52 | xit 'updates the favorite food' do 53 | # Write a test that will pass. 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/02_array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Array do 4 | # An implicitly defined 'subject' is available when the outermost example 5 | # group is a class. The 'subject' will be an instance of that class. 6 | # https://rspec.info/features/3-12/rspec-core/subject/implicit-subject/ 7 | 8 | # Note: Using an implicit subject is not recommended for most situations. 9 | # The next lesson will cover explicit subjects, which are recommended over 10 | # implicit subjects. 11 | 12 | context 'when subject is implicitly defined' do 13 | # Type matchers can use be_a or be_an to increase readability. 14 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/types/ 15 | it 'is an Array' do 16 | expect(subject).to be_an(Array) 17 | end 18 | 19 | # Below is one-line syntax that does the same as the above test. 20 | # Look at the doc string that is auto-generated when this test is run 21 | # (in a terminal window). 22 | it { is_expected.to be_an(Array) } 23 | end 24 | 25 | context 'when using predicate matchers' do 26 | context 'when using the empty? predicate method' do 27 | # A predicate method in Ruby ends with a ? and only returns true or false. 28 | it 'returns true' do 29 | expect(subject.empty?).to eq true 30 | end 31 | end 32 | 33 | # RSpec can leverage this to create predicate matchers for any predicate method. 34 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/predicates/ 35 | it 'is empty' do 36 | expect(subject).to be_empty 37 | end 38 | 39 | # Below is one-line syntax that does the same as the above test. 40 | # Look at the doc string that is auto-generated when this test is run 41 | # (in a terminal window). 42 | it { is_expected.to be_empty } 43 | end 44 | 45 | context 'when a let variable is declared inside a context block' do 46 | let(:numbers) { [3, 8, 9] } 47 | 48 | it 'has length of 3' do 49 | expect(numbers.length).to eq(3) 50 | end 51 | 52 | it 'changes the length to 2' do 53 | numbers.pop 54 | expect(numbers.length).to eq(2) 55 | end 56 | end 57 | 58 | # Look at the order of the above 5 tests when this test file is run 59 | # (in a terminal). 60 | # Why do you think they output in a different order than they are written? 61 | 62 | # The answer is that each group runs its examples before running its nested 63 | # example groups, even if the nested groups are defined before the examples. 64 | # https://rspec.info/features/3-12/rspec-core/command-line/order/ 65 | 66 | # Please note: one-line tests are only recommended when the matcher aligns 67 | # exactly with the doc string. Even in that case, many rubyists prefer 68 | # explicitly writing out the test & not using one-line syntax. 69 | end 70 | 71 | # ASSIGNMENT 72 | describe Array do 73 | context 'when updating an implicit subject' do 74 | # remove the 'x' before running this test 75 | xit 'is empty' do 76 | # Write a test to expect the subject to be empty. 77 | end 78 | 79 | # remove the 'x' before running this test 80 | xit 'updates length to 1' do 81 | # Update the implicit subject to make this test pass. 82 | expect(subject.length).to eq(1) 83 | end 84 | end 85 | 86 | context 'when using one let variable on two tests' do 87 | # Make a let variable that will pass both tests. 88 | 89 | # remove the 'x' before running this test 90 | xit 'has length of 3' do 91 | expect(lucky_numbers.length).to eq(3) 92 | end 93 | 94 | # remove the 'x' before running this test 95 | xit 'has sum of 42' do 96 | expect(lucky_numbers.sum).to eq(42) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/03_number_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # In a typical workflow, the class being tested will be located in a separate 4 | # file from the tests. But for this simple example, let's keep everything together. 5 | class SingleDigit 6 | attr_reader :number 7 | 8 | def initialize 9 | @number = rand(1..9) 10 | end 11 | end 12 | 13 | describe SingleDigit do 14 | # There are differences between let and subject that you can research. 15 | # In general, the subject is used to declare the test subject. 16 | # https://stackoverflow.com/questions/38437162/whats-the-difference-between-rspecs-subject-and-let-when-should-they-be-used 17 | 18 | # It is recommended to explicitly define the subject with a descriptive name. 19 | # Then use the descriptive name, instead of 'subject,' in the tests. 20 | # https://rspec.info/features/3-12/rspec-core/subject/explicit-subject/ 21 | subject(:random_digit) { SingleDigit.new } 22 | 23 | # There can be multiple tests in one example block; however, it is recommended 24 | # to test only one thing at a time. 25 | it 'is greater than or equal to 1' do 26 | # Comparison matchers are used with 'be' matcher 27 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/comparisons/ 28 | expect(random_digit.number).to be >= 1 29 | end 30 | 31 | it 'is less than 10' do 32 | expect(random_digit.number).to be < 10 33 | end 34 | 35 | # The above 2 tests can be compounded together, so that you test two things. 36 | # in one test at the same time. 37 | context 'when tests can be compounded' do 38 | it 'is greater than 0 and less than 10' do 39 | # side note: rspec runs, but rubocop does not like (be >= 1).and be < 10 40 | expect(random_digit.number).to be_positive.and be < 10 41 | end 42 | end 43 | 44 | # Instead of using .to, you can use the inverse built in matcher .not_to 45 | context 'when using not_to' do 46 | it 'is not equal to 10' do 47 | expect(random_digit.number).not_to eq(10) 48 | end 49 | 50 | it 'is not equal to 0' do 51 | expect(random_digit.number).not_to be_zero 52 | end 53 | 54 | it 'is not nil' do 55 | expect(random_digit.number).not_to be_nil 56 | end 57 | end 58 | end 59 | 60 | # ASSIGNMENT 61 | 62 | describe Array do 63 | context 'when my_array has perfect values' do 64 | # Write a subject variable 'my_array' that passes all tests. 65 | 66 | # remove the 'x' before running this test 67 | xit 'has a specific first value' do 68 | expect(my_array.first).to be_odd.and be <= -1 69 | end 70 | 71 | # remove the 'x' before running this test 72 | xit 'has a specific last value' do 73 | expect(my_array.last).to be_even.and be < 99 74 | end 75 | 76 | # remove the 'x' before running this test 77 | xit 'has a specific min value' do 78 | expect(my_array.min).not_to be < -9 79 | end 80 | 81 | # remove the 'x' before running this test 82 | xit 'has a specific max value' do 83 | expect(my_array.max).to be > 100 84 | end 85 | 86 | # remove the 'x' before running this test 87 | xit 'includes a value of 42' do 88 | expect(my_array).to include(42) 89 | end 90 | 91 | # remove the 'x' before running this test 92 | xit 'has a fourth element' do 93 | expect(my_array[3]).not_to be_nil 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/04_hash_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Hash do 4 | subject(:favorites) { { color: 'blue', food: 'fajitas' } } 5 | 6 | # As you discovered in the last assignment, the include matcher works on any 7 | # object that would respond to the #include? method. 8 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/include/ 9 | context 'when changing favorite color to forest green' do 10 | it 'includes green' do 11 | favorites[:color] = 'forest green' 12 | expect(favorites[:color]).to include('green') 13 | end 14 | end 15 | 16 | context 'when a favorite movie is not in the hash' do 17 | it 'is nil' do 18 | expect(favorites[:movie]).to be_nil 19 | end 20 | end 21 | 22 | # Use the 'be' matcher when testing for true or false values. 23 | context 'when testing for true or false values' do 24 | subject(:car_features) do 25 | { 26 | remote_start?: true, 27 | parking_camera?: true, 28 | assisted_steering?: false 29 | } 30 | end 31 | 32 | it 'has remote start' do 33 | expect(car_features[:remote_start?]).to be true 34 | end 35 | 36 | it 'does not have assisted steering' do 37 | expect(car_features[:assisted_steering?]).to be false 38 | end 39 | end 40 | end 41 | 42 | # ASSIGNMENT 43 | 44 | describe Hash do 45 | subject(:my_car) do 46 | { 47 | make: 'Volkswagen', 48 | model: 'Jetta', 49 | year: 2017, 50 | parking_camera?: true, 51 | assisted_steering?: false 52 | } 53 | end 54 | 55 | # remove the 'x' before running this test 56 | xit 'is newer than 2015' do 57 | # Write a test that verifies the above statement. 58 | end 59 | 60 | # remove the 'x' before running this test 61 | xit 'has a parking camera' do 62 | # Write a test that verifies the above statement. 63 | end 64 | 65 | # remove the 'x' before running this test 66 | xit 'does not have assisted steering' do 67 | # Write a test that verifies the above statement. 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/05_truthy_falsy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Do not use 'be_truthy' or 'be_falsy' for methods that should only be 4 | # evaluated into a Boolean. 5 | # https://luten.dev/rspec-be_truthy-exists-or-be-true/ 6 | 7 | describe 'truthy and falsy' do 8 | context 'almost everything is truthy' do 9 | it 'is truthy' do 10 | expect('foo').to be_truthy 11 | end 12 | 13 | it 'is truthy' do 14 | expect(7).to be_truthy 15 | end 16 | 17 | it 'is truthy' do 18 | expect(0).to be_truthy 19 | end 20 | 21 | it 'is truthy' do 22 | expect(-100).to be_truthy 23 | end 24 | 25 | it 'is truthy' do 26 | expect(3.14159).to be_truthy 27 | end 28 | 29 | it 'is truthy' do 30 | expect([]).to be_truthy 31 | end 32 | 33 | it 'is truthy' do 34 | expect({}).to be_truthy 35 | end 36 | 37 | it 'is truthy' do 38 | expect(:any_symbol).to be_truthy 39 | end 40 | 41 | it 'is truthy' do 42 | expect(true).to be_truthy 43 | end 44 | end 45 | 46 | context 'only false and nil are falsy' do 47 | it 'is falsy' do 48 | expect(false).to be_falsy 49 | end 50 | 51 | it 'is falsy' do 52 | expect(nil).to be_falsy 53 | end 54 | end 55 | end 56 | 57 | # There is no assignment for this lesson. Please continue to the next lesson. 58 | -------------------------------------------------------------------------------- /spec/06_equality_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 'eq' checks for equal VALUE. 4 | # 'eql' checks for equal VALUE and TYPE. 5 | # 'equal' checks for OBJECT IDENTITY. 6 | # 'be' checks for OBJECT IDENTITY. 7 | 8 | describe 'differences between eq, eql, equal, and be' do 9 | context 'my_score vs. your_score' do 10 | let(:my_score) { 10.0 } 11 | let(:your_score) { 10 } 12 | 13 | context 'eq only looks at value' do 14 | it 'is eq to each other' do 15 | expect(my_score).to eq(your_score) 16 | end 17 | end 18 | 19 | # my_score is a Float and your_score is an Integer. 20 | context 'eql looks at type & value' do 21 | it 'is not eql to each other' do 22 | expect(my_score).not_to eql(your_score) 23 | end 24 | end 25 | end 26 | 27 | context 'my_car vs. your_car vs. my_kids_borrow' do 28 | let(:my_car) { [2017, 'red', 'Jetta'] } 29 | let(:your_car) { [2017, 'red', 'Jetta'] } 30 | let(:my_kids_borrow) { my_car } 31 | 32 | context 'eql looks at type & value' do 33 | it 'is eql to each other' do 34 | expect(my_car).to eql(your_car) 35 | end 36 | 37 | it 'is eql to each other' do 38 | expect(my_kids_borrow).to eql(your_car) 39 | end 40 | end 41 | 42 | # Some prefer to use 'be' over 'equal' because it semantically makes sense. 43 | # expect(first_item).to be(second_item) 44 | context 'be and equal care about object identity' do 45 | it 'is comparing the same car' do 46 | expect(my_car).to equal(my_kids_borrow) 47 | end 48 | 49 | it 'is comparing the same car' do 50 | expect(my_car).to be(my_kids_borrow) 51 | end 52 | 53 | it 'is not comparing the same car' do 54 | expect(my_car).not_to be(your_car) 55 | end 56 | 57 | it 'is not comparing the same car' do 58 | expect(my_car).not_to equal([2017, 'red', 'Jetta']) 59 | end 60 | end 61 | end 62 | end 63 | 64 | # ASSIGNMENT 65 | 66 | describe 'equality assignment' do 67 | let(:amy) { { fav_color: 'blue', fav_food: 'tacos' } } 68 | let(:bob) { { fav_color: 'blue', fav_food: 'tacos' } } 69 | let(:copy_cat) { amy } 70 | # Write a test that expresses each of the following statements. 71 | 72 | # remove the 'x' before running this test 73 | xit 'amy is eq to bob' do 74 | end 75 | 76 | # remove the 'x' before running this test 77 | xit 'amy is eql to bob' do 78 | end 79 | 80 | # remove the 'x' before running this test 81 | xit 'amy is not equal to bob' do 82 | end 83 | 84 | # remove the 'x' before running this test 85 | xit 'copy_cat is equal to amy' do 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/07_all_contain_exactly_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The 'all' matcher and the 'contain_exactly' matcher each look at every 4 | # item in 'numbers'. 5 | 6 | describe Array do 7 | subject(:numbers) { [11, 17, 21] } 8 | 9 | it 'is all odd numbers' do 10 | expect(numbers).to all(be_odd) 11 | end 12 | 13 | it 'is all under 25' do 14 | expect(numbers).to all(be < 25) 15 | end 16 | 17 | it 'contains exactly 21, 11, 17' do 18 | # The order does not matter. 19 | expect(numbers).to contain_exactly(21, 11, 17) 20 | end 21 | end 22 | 23 | describe String do 24 | subject(:sample_word) { 'spaceship' } 25 | 26 | context 'when using start_with' do 27 | it 'starts with s' do 28 | expect(sample_word).to start_with('s') 29 | end 30 | 31 | it 'starts with spa' do 32 | expect(sample_word).to start_with('spa') 33 | end 34 | 35 | it 'starts with space' do 36 | expect(sample_word).to start_with('space') 37 | end 38 | 39 | it 'starts with the whole word' do 40 | expect(sample_word).to start_with('spaceship') 41 | end 42 | end 43 | 44 | context 'when using end_with' do 45 | it 'ends with p' do 46 | expect(sample_word).to end_with('p') 47 | end 48 | 49 | it 'ends with hip' do 50 | expect(sample_word).to end_with('hip') 51 | end 52 | 53 | it 'ends with ship' do 54 | expect(sample_word).to end_with('ship') 55 | end 56 | 57 | it 'ends with the whole word' do 58 | expect(sample_word).to end_with('spaceship') 59 | end 60 | end 61 | end 62 | 63 | describe Array do 64 | subject(:symbol_array) { %i[a b c d e] } 65 | 66 | it 'starts with a and ends with e' do 67 | expect(symbol_array).to start_with(:a).and end_with(:e) 68 | end 69 | 70 | it 'starts with a and includes c' do 71 | expect(symbol_array).to start_with(:a).and include(:c) 72 | end 73 | end 74 | 75 | # ASSIGNMENT 76 | 77 | describe Array do 78 | subject(:fibonacci_sequence) { [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] } 79 | # Write a test that expresses each of the following statements. 80 | 81 | # remove the 'x' before running this test 82 | xit 'includes 21 and ends with 89' do 83 | end 84 | 85 | # remove the 'x' before running this test 86 | xit 'starts with 0, 1, 1, 2 and all are under 100' do 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/08_change_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Array do 4 | subject(:drinks) { %w[coffee tea water] } 5 | 6 | # Using .or instead of .and when compounding matchers: 7 | context 'when testing for multiple outcomes' do 8 | it 'will be coffee, tea, or water' do 9 | expect(drinks.sample).to eq('coffee').or eq('tea').or eq('water') 10 | end 11 | end 12 | 13 | # When testing for a change to occur, notice that unlike previous matchers 14 | # we've seen, 'change' accepts a block of code. 15 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/change/ 16 | 17 | context 'when testing for a change' do 18 | it 'will change the length to 4' do 19 | expect { drinks << 'juice' }.to change { drinks.length }.to(4) 20 | end 21 | 22 | it 'will change the length from 3 to 4' do 23 | expect { drinks << 'juice' }.to change { drinks.length }.from(3).to(4) 24 | end 25 | 26 | # The above two tests are too tightly coupled to a specific array length. 27 | # The test should instead be written for any length of array, for example: 28 | it 'will increase the length by one' do 29 | expect { drinks << 'juice' }.to change { drinks.length }.by(1) 30 | end 31 | 32 | # There are additional ways to be more descriptive about the change. 33 | it 'will increase the length by at most one' do 34 | expect { drinks << 'juice' }.to change { drinks.length }.by_at_most(1) 35 | end 36 | 37 | # Alternate form for 'change' matcher using (object, :attribute): 38 | it 'will increase the length by one' do 39 | expect { drinks << 'juice' }.to change(drinks, :length).by(1) 40 | end 41 | 42 | # You can compound change matchers together. 43 | it 'will decrease by one and end with tea' do 44 | expect { drinks.pop }.to change { drinks.length }.by(-1).and change { drinks.last }.to('tea') 45 | end 46 | end 47 | end 48 | 49 | # ASSIGNMENT: 50 | 51 | describe 'lucky numbers with rotate! method' do 52 | subject(:lucky_numbers) { [3, 7, 13, 31, 42] } 53 | # Write a test that expresses each of the following statements. 54 | # Use the array #rotate! method to manipulate lucky_numbers. 55 | 56 | context 'when rotating the array of lucky numbers' do 57 | # remove the 'x' before running this test 58 | xit 'will change the first value to 7' do 59 | end 60 | 61 | # remove the 'x' before running this test 62 | xit 'will change the last value to 3' do 63 | end 64 | 65 | # remove the 'x' before running this test 66 | xit 'will change the first value to 7 and last value to 3' do 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/09_custom_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # If you need to test a condition that does not have a built-in matcher, 4 | # you can create your own. 5 | # https://rspec.info/features/3-12/rspec-expectations/custom-matchers/ 6 | 7 | describe 'defining custom matchers' do 8 | context 'when reusing a matcher that is in scope' do 9 | matcher :be_divisible_by_four do 10 | match { |num| (num % 4).zero? } 11 | end 12 | 13 | it 'is divisible by 4' do 14 | expect(12).to be_divisible_by_four 15 | end 16 | 17 | # You can test for the inverse of the matcher. 18 | it 'is not divisible by 4' do 19 | expect(99).not_to be_divisible_by_four 20 | end 21 | 22 | # You can even use a custom matcher with 'all'. 23 | it 'works with multiple values' do 24 | expect([12, 100, 800]).to all(be_divisible_by_four) 25 | end 26 | end 27 | end 28 | 29 | # ASSIGNMENT 30 | 31 | describe 'one word palindrome test' do 32 | let(:racecar) { 'racecar' } 33 | let(:spaceship) { 'spaceship' } 34 | let(:rotator) { 'rotator' } 35 | let(:palindrome) { 'palindrome' } 36 | 37 | # Write a custom matcher that detects a one word palindrome, 38 | # using the following block: { |word| word.reverse == word } 39 | # When it is set up correctly, all of the following tests will pass. 40 | 41 | context 'when a palindrome is used' do 42 | # remove the 'x' before running this test 43 | xit 'is a palindrome' do 44 | expect(racecar).to be_a_palindrome 45 | end 46 | 47 | # remove the 'x' before running this test 48 | xit 'is a palindrome' do 49 | expect(rotator).to be_a_palindrome 50 | end 51 | end 52 | 53 | context 'when a palindrome is not used' do 54 | # remove the 'x' before running this test 55 | xit 'is not a palindrome' do 56 | expect(spaceship).not_to be_a_palindrome 57 | end 58 | 59 | # remove the 'x' before running this test 60 | xit 'is not a palindrome' do 61 | expect(palindrome).not_to be_a_palindrome 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/10_drink_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Now that the basics are covered, we are going to use a typical workflow 4 | # for the rest of the lessons. The class being tested will be located in 5 | # a different file. 6 | 7 | # The file order to complete this lesson: 8 | # 1. Familarize yourself with the class in lib/10_drink.rb 9 | # 2. Complete spec/10_drink_spec.rb 10 | 11 | # If you are using VS Code, you can split the screen to see both files at 12 | # the same time (view menu -> editor layout). 13 | 14 | require_relative '../lib/10_drink' 15 | 16 | describe Drink do 17 | describe '#initialize' do 18 | # The Drink class needs to have an attr_reader for both :type and :ounces. 19 | context 'when using default initialization' do 20 | subject(:default_drink) { Drink.new } 21 | 22 | it 'is water' do 23 | expect(default_drink.type).to eq('water') 24 | end 25 | 26 | it 'has 16 ounces' do 27 | expect(default_drink.ounces).to eq(16) 28 | end 29 | end 30 | 31 | context 'when specifying the type and ounces' do 32 | subject(:my_coffee) { Drink.new('coffee', 8) } 33 | 34 | it 'is coffee' do 35 | expect(my_coffee.type).to eq('coffee') 36 | end 37 | 38 | it 'has 8 ounces' do 39 | expect(my_coffee.ounces).to eq(8) 40 | end 41 | end 42 | 43 | # Use 'described_class' instead of the class name to limit the code that 44 | # needs to be changed, if/when it changes. For example, as applications 45 | # develop, class names are subject to change, so that Drink could be 46 | # changed to 'Beverage'. If that change were made, every time the word 47 | # 'Drink' was used, it would have to be changed to 'Beverage'. 48 | 49 | context 'when limiting future code changes' do 50 | subject(:my_drink) { described_class.new('juice') } 51 | 52 | it 'is juice' do 53 | expect(my_drink.type).to eq('juice') 54 | end 55 | 56 | it 'has 16 ounces' do 57 | expect(my_drink.ounces).to eq(16) 58 | end 59 | end 60 | end 61 | 62 | describe '#full?' do 63 | context 'when using magic matchers' do 64 | # When using a method that returns a boolean value & does not take any 65 | # parameters, you can use magic matchers. 66 | # http://testing-for-beginners.rubymonstas.org/rspec/matchers.html 67 | 68 | context 'when using default initialization' do 69 | subject(:default_drink) { described_class.new } 70 | 71 | it 'is full' do 72 | expect(default_drink).to be_full 73 | end 74 | end 75 | 76 | context 'when drink has 8 ounces' do 77 | subject(:my_coffee) { described_class.new('coffee', 8) } 78 | 79 | it 'is not full' do 80 | expect(my_coffee).not_to be_full 81 | end 82 | end 83 | end 84 | end 85 | end 86 | 87 | # ASSIGNMENT 88 | 89 | describe Drink do 90 | describe '#initialize' do 91 | context 'when type is specified and ounces is default' do 92 | # Create an explicit subject, using 'described_class' and your choice of 93 | # beverage type. 94 | 95 | # remove the 'x' before running this test 96 | xit 'is your choice of beverage' do 97 | end 98 | 99 | # remove the 'x' before running this test 100 | xit 'has 16 ounces' do 101 | end 102 | end 103 | end 104 | 105 | describe '#full?' do 106 | context 'when drink has 16 ounces or more' do 107 | # Create an explicit subject, using 'described_class' and your choice of 108 | # beverage type. 109 | 110 | # remove the 'x' before running this test 111 | xit 'is full' do 112 | end 113 | end 114 | 115 | context 'when drink has less than 16 ounces' do 116 | # Create an explicit subject, using 'described_class' and your choice of 117 | # beverage type. In addition, specify ounces to be any number under 16. 118 | 119 | # remove the 'x' before running this test 120 | xit 'is not full' do 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/11a_shared_example_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The file order to complete this lesson: 4 | 5 | # 1. Familiarize yourself with the three lib/11 files. 6 | # - lib/11a_pet.rb is the parent 'Pet' class of 'Cat' and 'Dog' 7 | # - lib/11b_cat.rb is a subclass 'Cat' 8 | # - lib/11c_dog.rb is a subclass 'Dog' 9 | # 2. Review the tests in spec/11a_shared_example_spec.rb 10 | # 3. Complete either spec/11b_cat_spec.rb or spec/11c_dog_spec.rb 11 | 12 | # Start with reading about the use of shared examples: 13 | # https://rspec.info/features/3-12/rspec-core/example-groups/shared-examples/ 14 | 15 | # These tests are dependent on using the implicit 'subject' when they are 16 | # included in another spec file. This file is not intended to be used alone. 17 | # If you run rspec on this file, there will be 'no examples found' because 18 | # there is not an implicit subject available. 19 | 20 | RSpec.shared_examples 'base class method name' do 21 | # This test can be used in Cat and Dog because the method comes from the 22 | # base class. 23 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/respond-to/ 24 | context 'when method is from the base class' do 25 | it 'responds to meal_time' do 26 | expect(subject).to respond_to(:meal_time) 27 | end 28 | end 29 | end 30 | 31 | RSpec.shared_examples 'shared method name' do 32 | # This test can be used in Cat and Dog because the same method name is used 33 | # in both classes ('meow' or 'WOOF!'). 34 | context 'when method name is the same in multiple classes' do 35 | it 'responds to talk' do 36 | expect(subject).to respond_to(:talk) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/11b_cat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11b_cat' 4 | require_relative '../spec/11a_shared_example_spec' 5 | 6 | # The file order to complete this lesson: 7 | 8 | # 1. Familiarize yourself with the three lib/11 files. 9 | # - lib/11a_pet.rb is the parent 'Pet' class of 'Cat' and 'Dog' 10 | # - lib/11b_cat.rb is a subclass 'Cat' 11 | # - lib/11c_dog.rb is a subclass 'Dog' 12 | # 2. Review the tests in spec/11a_shared_example_spec.rb 13 | # 3. Complete either spec/11b_cat_spec.rb or spec/11c_dog_spec.rb 14 | 15 | describe Cat do 16 | subject(:oscar) { described_class.new('Oscar', 'Maine Coon') } 17 | 18 | # Before you begin this file, make sure you have read the shared 19 | # example file: 11a_shared_examples_spec.rb. This test references 20 | # that file's first test in the below 'include_examples' line. 21 | context 'when Cat is a child class of Pet' do 22 | include_examples 'base class method name' 23 | end 24 | 25 | context 'when cat has name and breed, but no color' do 26 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/have-attributes/ 27 | it 'has name, breed & color attributes' do 28 | expect(oscar).to have_attributes(name: 'Oscar', breed: 'Maine Coon', color: nil) 29 | end 30 | end 31 | end 32 | 33 | # ASSIGNMENT - complete either Cat or Dog assignment 34 | # (see 11c_dog_spec.rb for Dog assignment) 35 | 36 | describe Cat do 37 | # Create a subject with your choice of cat name and optional breed/color. 38 | 39 | # Write a test using the second shared_example to test that cat responds to 40 | # talk ('meow'). 41 | context '' do 42 | end 43 | 44 | # remove the 'x' before running this test 45 | xit 'is not hungry' do 46 | end 47 | 48 | # remove the 'x' before running this test 49 | xit 'is hiding' do 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/11c_dog_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11c_dog' 4 | require_relative '../spec/11a_shared_example_spec' 5 | 6 | # The file order to complete this lesson: 7 | 8 | # 1. Familiarize yourself with the three lib/11 files. 9 | # - lib/11a_pet.rb is the parent 'Pet' class of 'Cat' and 'Dog' 10 | # - lib/11b_cat.rb is a subclass 'Cat' 11 | # - lib/11c_dog.rb is a subclass 'Dog' 12 | # 2. Review the tests in spec/11a_shared_example_spec.rb 13 | # 3. Complete either spec/11b_cat_spec.rb or spec/11c_dog_spec.rb 14 | 15 | describe Dog do 16 | subject(:toby) { described_class.new('Toby', nil, 'brown') } 17 | 18 | # Before you begin this file, make sure you have read the shared 19 | # example file: 11a_shared_examples_spec.rb. This test references 20 | # that file's first test in the below 'include_examples' line. 21 | context 'when Dog is a child class of Pet' do 22 | include_examples 'base class method name' 23 | end 24 | 25 | context 'when dog has name and color, but no breed' do 26 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/have-attributes/ 27 | it 'has name, breed & color attributes' do 28 | expect(toby).to have_attributes(name: 'Toby', breed: nil, color: 'brown') 29 | end 30 | end 31 | end 32 | 33 | # ASSIGNMENT - complete either Cat or Dog assignment 34 | # (see 11b_cat_spec.rb for Cat assignment) 35 | 36 | describe Dog do 37 | # Create a subject with your choice of dog name and optional breed/color. 38 | 39 | # Write a test using the second shared_example to test that dog responds to 40 | # talk ('WOOF!'). 41 | context '' do 42 | end 43 | 44 | # remove the 'x' before running this test 45 | xit 'is not barking' do 46 | end 47 | 48 | # remove the 'x' before running this test 49 | xit 'is sleeping' do 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/12_magic_seven_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/12_magic_seven' 4 | 5 | # The file order to complete this lesson: 6 | # 1. Familiarize yourself with the class in lib/12_magic_seven.rb 7 | # 2. Complete spec/12_magic_seven_spec.rb 8 | 9 | # Before learning any more complexities of testing, let's take a look at a 10 | # standard testing pattern: Arrange, Act, and Assert. 11 | # https://youtu.be/sCthIEOaMI8 12 | 13 | # 1. Arrange -> set up the test (examples: initializing objects, let 14 | # variables, updating values of instance variables). 15 | # 2. Act -> execute the logic to test (example: calling a method to run). 16 | # 3. Assert -> expect the results of arrange & act. 17 | 18 | # The tests in this lesson are fairly easy to understand, and it may seem 19 | # ridiculous to use A-A-A for them. However, tests should be easily understood 20 | # not just by you, but also by someone that is not familiar with the code. 21 | 22 | # NOTE: When you start using A-A-A to format your tests, it will feel 23 | # strange to not be following DRY (Don't Repeat Yourself). With tests, however, 24 | # repetition is necessary in order for them to be easy to read. If you are using 25 | # rubocop, you can disable specific (or all) cops for certain files (or 26 | # directories) by adding a .rubocop.yml file. 27 | # https://docs.rubocop.org/rubocop/0.88/configuration.html#includingexcluding-files 28 | 29 | # When you start working on an existing code base, you will often become familiar 30 | # with the code by reading the tests. 31 | 32 | describe MagicSeven do 33 | # This next line should be very familiar, and it is part of the 'Arrange' step. 34 | subject(:game) { described_class.new } 35 | 36 | describe '#add_nine' do 37 | # This test could be written as below (and it would pass): 38 | it 'returns 15' do 39 | expect(game.add_nine(6)).to eq(15) 40 | end 41 | 42 | # However, the above test is NOT very readable. For example, it does not 43 | # explain where '6' came from. So let's start with explaining 44 | # where '6' came from, as part of the 'Arrange' step. 45 | it 'returns 15' do 46 | random_number = 6 47 | 48 | # For the 'Act' step, we will be testing the result of the logic of adding 49 | # nine to the random_number. 50 | result = game.add_nine(random_number) 51 | 52 | # For the 'Assert' step, we know exactly what we expect the result to be: 53 | expect(result).to eq(15) 54 | end 55 | end 56 | 57 | # In addition, using a context to explain the conditions of the test makes 58 | # the output more readable. 59 | describe '#multiply_by_two' do 60 | context 'when the previous step is 8' do 61 | it 'returns 16' do 62 | previous_step = 8 # Arrange 63 | result = game.multiply_by_two(previous_step) # Act 64 | expect(result).to eq(16) # Assert 65 | end 66 | end 67 | end 68 | 69 | # ASSIGNMENT 70 | # Write a test for each of the following methods: 71 | 72 | describe '#subtract_four' do 73 | end 74 | 75 | describe '#divide_by_two' do 76 | end 77 | 78 | # The following tests will need you to create new instances of MagicSeven with 79 | # a specific value for the random_number. 80 | describe '#subtract_random_number' do 81 | end 82 | 83 | # The #play method will always return seven! Test this game, using any 84 | # integer as the random_number. Update the context with the number. 85 | describe '#play' do 86 | context 'when the random number is ...' do 87 | # remove the 'x' before running this test 88 | xit 'will return 7' do 89 | end 90 | end 91 | 92 | context 'when the random number is ...' do 93 | # remove the 'x' before running this test 94 | xit 'will return 7' do 95 | end 96 | end 97 | 98 | context 'when the random number is ...' do 99 | # remove the 'x' before running this test 100 | xit 'will return 7' do 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/13_input_output_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/13_input_output' 4 | 5 | # The file order to complete this lesson: 6 | # 1. Familiarize yourself with the two classes in lib/13_input_output.rb 7 | # 2. Complete spec/13_input_output_spec.rb 8 | 9 | # Ruby code that was written before you learned how to use RSpec may be nearly 10 | # impossible to test. For example, in the lib/13_input_output file, there are 11 | # two identical games: ImpossibleToTestGame and NumberGame. Take a look at each 12 | # game and look for differences that may make one easier or harder to test 13 | # than the other. 14 | 15 | # One key difference between the two is that NumberGame has smaller, 16 | # isolated methods. 17 | 18 | # Small and isolated methods that only do one thing are easier to test. 19 | # Long methods are like a run-on sentence that should have been divided into 2 or 3 different sentences so that everything could be clearly understood and in this case if a method does many different things it can be difficult to test. 20 | 21 | # So if you are new to testing, be open to refactoring your previous 22 | # code, to make writing tests easier. As you learn testing, you will also 23 | # learn how to write better testable methods. 24 | 25 | # The tests in this file are longer than those in the previous lessons. 26 | # To get your bearings, remember to reference the following lines: 27 | # describe -> Name of the method that is being tested. 28 | # context -> Explains the conditions of the test. 29 | # it -> Explains the results of the test. 30 | 31 | describe NumberGame do 32 | subject(:game) { described_class.new } 33 | 34 | describe '#initialize' do 35 | context 'when solution is initialized' do 36 | it 'is a number greater than or equal to 0' do 37 | solution = game.solution 38 | expect(solution).to be >= 0 39 | end 40 | 41 | it 'is a number less than or equal to 9' do 42 | solution = game.solution 43 | expect(solution).to be <= 9 44 | end 45 | 46 | # ASSIGNMENT #1 47 | 48 | # Write a similar test to the one above, that uses a custom matcher 49 | # instead of <, >, =. 50 | matcher :be_between_zero_and_nine do 51 | end 52 | 53 | # remove the 'x' before running this test 54 | xit 'is a number between 0 and 9' do 55 | end 56 | end 57 | end 58 | 59 | describe '#game_over?' do 60 | context 'when user guess is correct' do 61 | # To test this method, we need to set specific values for @solution and 62 | # @guess, so we will create a new instance of NumberGame. When creating 63 | # another instance of NumberGame, we'll use a meaningful name to 64 | # differentiate between instances. 65 | 66 | # A helpful tip is to combine the purpose of the test and the object. 67 | # E.g., game_end or complete_game. 68 | 69 | subject(:game_end) { described_class.new(5, '5') } 70 | 71 | it 'is game over' do 72 | expect(game_end).to be_game_over 73 | end 74 | end 75 | 76 | # ASSIGNMENT #2 77 | 78 | # Create a new instance of NumberGame and write a test for when the @guess 79 | # does not equal @solution. 80 | context 'when user guess is not correct' do 81 | # remove the 'x' before running this test 82 | xit 'is not game over' do 83 | end 84 | end 85 | end 86 | 87 | # The #player_input method is used in the game as an argument passed into the 88 | # verify_input method. The #player_input method is not tested because it is a 89 | # private method. In addition, it is unnecessary to test methods that only 90 | # contain puts and/or gets. 91 | 92 | # Since we do not have to test #player_input, let's test #verify_input. 93 | 94 | describe '#verify_input' do 95 | subject(:game_check) { described_class.new } 96 | # Note: #verify_input will only return a value if it matches /^[0-9]$/ 97 | 98 | context 'when given a valid input as argument' do 99 | it 'returns valid input' do 100 | user_input = '3' 101 | verified_input = game_check.verify_input(user_input) 102 | expect(verified_input).to eq('3') 103 | end 104 | end 105 | 106 | # ASSIGNMENT #3 107 | 108 | # Write a test for the following context. 109 | context 'when given invalid input as argument' do 110 | xit 'returns nil' do 111 | end 112 | end 113 | end 114 | 115 | describe '#player_turn' do 116 | # In order to test the behavior of #player_turn, we need to use a method 117 | # stub for #player_input to return a valid_input ('3'). To stub a method, 118 | # we 'allow' the test subject (game_loop) to receive the :method_name 119 | # and to return a specific value. 120 | # https://rspec.info/features/3-12/rspec-mocks/basics/allowing-messages/ 121 | # http://testing-for-beginners.rubymonstas.org/test_doubles.html 122 | # https://edpackard.medium.com/the-no-problemo-basic-guide-to-doubles-and-stubs-in-rspec-1af3e13b158 123 | 124 | subject(:game_loop) { described_class.new } 125 | 126 | context 'when user input is valid' do 127 | # To test the behavior, we want to test that the loop stops before the 128 | # puts 'Input error!' line. In order to test that this method is not 129 | # called, we use a message expectation. 130 | # https://rspec.info/features/3-12/rspec-mocks/ 131 | 132 | it 'stops loop and does not display error message' do 133 | valid_input = '3' 134 | allow(game_loop).to receive(:player_input).and_return(valid_input) 135 | # To use a message expectation, move 'Assert' before 'Act'. 136 | expect(game_loop).not_to receive(:puts).with('Input error!') 137 | game_loop.player_turn 138 | end 139 | end 140 | 141 | context 'when user inputs an incorrect value once, then a valid input' do 142 | # As the 'Arrange' step for tests grows, you can use a before hook to 143 | # separate the test from the set-up. 144 | # https://rspec.info/features/3-12/rspec-core/hooks/before-and-after-hooks/ 145 | # https://www.tutorialspoint.com/rspec/rspec_hooks.htm 146 | 147 | before do 148 | # A method stub can be called multiple times and return different values. 149 | # https://rspec.info/features/3-12/rspec-mocks/configuring-responses/returning-a-value/ 150 | # This method stub for :player_input will return the invalid 'letter' input, 151 | # then it will return the 'valid_input' 152 | letter = 'd' 153 | valid_input = '8' 154 | allow(game_loop).to receive(:player_input).and_return(letter, valid_input) 155 | end 156 | 157 | # When using message expectations, you can specify how many times you 158 | # expect the message to be received. 159 | # https://rspec.info/features/3-12/rspec-mocks/setting-constraints/receive-counts/ 160 | it 'completes loop and displays error message once' do 161 | expect(game_loop).to receive(:puts).with('Input error!').once 162 | game_loop.player_turn 163 | end 164 | end 165 | 166 | # ASSIGNMENT #4 167 | 168 | # Write a test for the following context. 169 | context 'when user inputs two incorrect values, then a valid input' do 170 | before do 171 | end 172 | 173 | xit 'completes loop and displays error message twice' do 174 | end 175 | end 176 | end 177 | 178 | # It is unneccessary to write tests for methods that only contain puts 179 | # statements, like #final_message. Puts is a basic part of the standard 180 | # ruby library & is already well tested. Plus, most 'real world 181 | # applications' don't even output like this except to loggers. 182 | 183 | # However, here is an example of how to test 'puts' using the output matcher. 184 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/output/ 185 | 186 | describe '#final_message' do 187 | context 'when count is 1' do 188 | # To test this method, we need to set specific values for @solution, 189 | # @guess and @count, so we will create a new instance of NumberGame. 190 | subject(:game_one) { described_class.new(5, '5', 1) } 191 | 192 | it 'outputs correct phrase' do 193 | lucky_phrase = "LUCKY GUESS!\n" 194 | # The output matcher needs a block of code to assert. 195 | expect { game_one.final_message }.to output(lucky_phrase).to_stdout 196 | end 197 | end 198 | 199 | # ASSIGNMENT #5 200 | 201 | # Create a new instance of NumberGame, with specific values for @solution, 202 | # @guess, and @count 203 | context 'when count is 2-3' do 204 | # remove the 'x' before running this test 205 | xit 'outputs correct phrase' do 206 | congrats_phrase = "Congratulations! You picked the random number in 3 guesses!\n" 207 | expect { game.final_message }.to output(congrats_phrase).to_stdout 208 | end 209 | end 210 | 211 | # ASSIGNMENT #6 212 | 213 | # Write a test for the following context. 214 | context 'when count is 4 and over' do 215 | # remove the 'x' before running this test 216 | xit 'outputs correct phrase' do 217 | end 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /spec/14_find_number_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/14_find_number' 4 | 5 | # The file order to complete this lesson: 6 | # 1. Familiarize yourself with the initialize method in lib/14_find_number.rb 7 | # 2. Start reading spec/14_find_number_spec.rb, which will also include 8 | # instructions to add methods to lib/14_find_number.rb 9 | 10 | # This file focuses on test-driven development (TDD). One important 11 | # TDD technique is using a 'double' for any object outside of the class being 12 | # tested. A 'double' is a generic ruby object that stands in for the real 13 | # object, like a stunt double. 14 | 15 | # Doubles are very useful in TDD because you can create test functionality that 16 | # is not coded yet with them. 17 | 18 | # In this lesson, we will be testing the class 'FindNumber'. Look at the 19 | # lib/14_find_number.rb file. An instance of 'FindNumber' is initialized with 20 | # min, max, answer and guess. There are default values for answer and guess. 21 | 22 | # Note: the 'RandomNumber' class has not been written. During TDD, we will need 23 | # to create a double for RandomNumber in the tests for FindNumber. 24 | # https://rspec.info/features/3-12/rspec-mocks/basics/test-doubles/ 25 | 26 | # Learning about doubles can be very confusing, because many resources use 27 | # doubles/mocks/stubs interchangeably. If you want to dig a little deeper, 28 | # here are a few additional resources to check out: 29 | # https://www.tutorialspoint.com/rspec/rspec_test_doubles.htm 30 | # https://www.codewithjason.com/rspec-mocks-stubs-plain-english/ 31 | 32 | describe FindNumber do 33 | # There are two ways to specify the messages that a double can receive. 34 | 35 | describe '#initialize' do 36 | # Doubles are strict, which means you must specify (allow) any messages 37 | # that they can receive. If a double receives a message that has not been 38 | # allowed, it will trigger an error. 39 | 40 | # This first example creates the double, then allows specific methods. 41 | context 'when creating the double and allowing method(s) in two steps' do 42 | let(:random_number) { double('random_number') } 43 | subject(:game) { described_class.new(0, 9, random_number) } 44 | 45 | context 'when random_number is 8' do 46 | it 'returns 8' do 47 | allow(random_number).to receive(:value).and_return(8) 48 | solution = game.answer 49 | expect(solution).to eq(8) 50 | end 51 | end 52 | end 53 | 54 | # This second example creates the double & specific methods together. 55 | context 'when creating the double and allowing method(s) in one step' do 56 | # A hash can be used to define allowed messages and return values. 57 | # When passing a hash as the last argument, the { } are not required. 58 | # let(:random_number) { double('random_number', { value: 3 }) } 59 | let(:random_number) { double('random_number', value: 3) } 60 | subject(:game) { described_class.new(0, 9, random_number) } 61 | 62 | context 'when random_number is 3' do 63 | it 'returns 3' do 64 | solution = game.answer 65 | expect(solution).to eq(3) 66 | end 67 | end 68 | end 69 | 70 | # When testing the same method in multiple tests, it is possible to add 71 | # method names to subject. 72 | context 'when adding method names to subject for multiple tests' do 73 | let(:random_number) { double('random_number', value: 5) } 74 | subject(:game_solution) { described_class.new(0, 9, random_number).answer } 75 | 76 | context 'when random_number is 5' do 77 | it 'returns 5' do 78 | expect(game_solution).to eq(5) 79 | end 80 | end 81 | end 82 | end 83 | end 84 | 85 | # ASSIGNMENT 86 | # For this lesson you will be doing TDD for 3 methods: #make_guess, 87 | # #game_over?, and #update_range. 88 | 89 | # After you have some experience using TDD, you can use the typical 90 | # Red-Green-Refactor workflow. 91 | # https://thoughtbot.com/upcase/videos/red-green-refactor-by-example 92 | 93 | # Since this is probably your first experience with TDD, let's extend the 94 | # workflow to include a few more steps: 95 | # 1. Read & understand the requirement for one method only. 96 | # 2. Write one test for that method; run the tests to see it fail. 97 | # 3. Write the method to fulfill the requirement. 98 | # 4. Run the tests again. If they don't all pass, redo steps 1-3. 99 | # 5. When your first test is passing, write the additional tests. 100 | # 6. Run all of the tests. If any do not pass, redo steps 3-5. 101 | # 7. Optional: Refactor your code and/or tests, keeping all tests passing. 102 | 103 | describe FindNumber do 104 | # ASSIGNMENT: METHOD #1 105 | 106 | # The basic idea of 'FindNumber' is to program a computer to guess a 107 | # random_number using binary search. Remember the binary search video 108 | # that you watched in the Computer Science section: 109 | # https://www.youtube.com/watch?v=T98PIp4omUA 110 | 111 | # The computer will update min and max values to help find the correct number. 112 | 113 | describe '#make_guess' do 114 | # Create a random_number double called 'number_guessing'. Allow the double 115 | # to receive 'value' and return the value of 8, in one of the two ways 116 | # explained above. 117 | 118 | subject(:game_guessing) { described_class.new(0, 9, number_guessing) } 119 | 120 | # Before you write the #make_guess method: 121 | # Write a test that would expect #make_guess to return the average of 122 | # the min and max values (rounded down). Don't expect this test to pass. 123 | # It will fail with an undefined method error because you haven't 124 | # written #make_guess yet! 125 | context 'when min is 0 and max is 9' do 126 | xit 'returns 4' do 127 | end 128 | end 129 | 130 | # Now write a method in 14_find_number.rb called #make_guess that returns 131 | # the average of the min and max values (rounded down). 132 | # You can now run the above test and it should pass. 133 | 134 | # Write a test for each of the following contexts. You will need to create a 135 | # new instance of FindNumber for each context, but you can use the same 136 | # random number double created inside this method's describe block. 137 | 138 | context 'when min is 5 and max is 9' do 139 | xit 'returns 7' do 140 | end 141 | end 142 | 143 | context 'when min is 8 and max is 9' do 144 | xit 'returns 8' do 145 | end 146 | end 147 | 148 | context 'when min is 0 and max is 3' do 149 | xit 'returns 1' do 150 | end 151 | end 152 | 153 | context 'when min and max both equal 3' do 154 | xit 'returns 3' do 155 | end 156 | end 157 | end 158 | 159 | # ASSIGNMENT: METHOD #2 160 | describe '#game_over?' do 161 | context 'when guess and random_number are equal' do 162 | # Create another subject and random_number double with meaningful names. 163 | # The subject will need to specify the number value of @guess. 164 | 165 | # Allow the double to receive 'value' and return the same number as @guess. 166 | 167 | # Write a test that would expect game to be_game_over when a guess equals 168 | # the random_number double's value above. Remember that this test will not 169 | # be able to pass yet because you haven't written the method! 170 | 171 | xit 'is game over' do 172 | end 173 | end 174 | 175 | # Write the corresponding method in 14_find_number.rb called #game_over? 176 | # that returns true when a guess equals the value of the random_number. 177 | 178 | # Write a test that would expect game to NOT be_game_over when a guess does 179 | # NOT equal the random_number double's value above. 180 | 181 | context 'when guess and random_number are not equal' do 182 | xit 'is not game over' do 183 | end 184 | end 185 | end 186 | 187 | # ASSIGNMENT: METHOD #3 188 | describe '#update_range' do 189 | # As you have seen above, you can share the same random_number double for 190 | # multiple context blocks, by declaring it inside the describe block. 191 | let(:number_range) { double('random_number', value: 8) } 192 | 193 | # Write four tests for #update_range that test the values of min and max. 194 | 195 | # When the guess is less than the answer: 196 | # - min updates to one more than the guess 197 | # - max stays the same 198 | 199 | # When the guess is more than the answer: 200 | # - min stays the same 201 | # - max updates to one less than the guess 202 | 203 | context 'when the guess is less than the answer' do 204 | subject(:low_guess_game) { described_class.new(0, 9, number_range, 4) } 205 | 206 | xit 'updates min to 5' do 207 | end 208 | 209 | xit 'does not update max' do 210 | end 211 | end 212 | 213 | context 'when the guess is more than the answer' do 214 | subject(:high_guess_game) { described_class.new(0, 9, number_range, 9) } 215 | 216 | xit 'does not update min' do 217 | end 218 | 219 | xit 'updates max to 8' do 220 | end 221 | end 222 | 223 | # Now, write the method in 14_find_number.rb called #update_range that will 224 | # do the following: 225 | 226 | # When the guess is less than the answer: 227 | # - min updates to one more than the guess 228 | 229 | # When the guess is not less than the answer: 230 | # - max updates to one less than the guess 231 | 232 | # Write a test for any 'edge cases' that you can think of, for example: 233 | 234 | context 'when the guess is 7, min=5, and max=8' do 235 | xit 'updates min to the same value as max' do 236 | end 237 | 238 | xit 'does not update max' do 239 | end 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /spec/15a_binary_game_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/15a_binary_game' 4 | require_relative '../lib/15b_binary_search' 5 | require_relative '../lib/15c_random_number' 6 | 7 | # The file order to complete this lesson: 8 | 9 | # 1. Familiarize yourself with the four lib/15 files. 10 | # - lib/15_binary_main 11 | # - lib/15a_binary_game 12 | # - lib/15b_binary_search which is based on 14_find_number 13 | # - lib/15c_random_number 14 | # 2. Read the comments in spec/15b_binary_search_spec 15 | # 3. Complete spec/15a_binary_game_spec 16 | 17 | # As noted above, the files for this lesson (#15) build on the TDD files 18 | # from #14. The FindNumber class is now called BinarySearch, which is a more 19 | # accurate description. 20 | 21 | # This lesson has a new class called BinaryGame. This BinaryGame class uses 22 | # the BinarySearch class and visualizes how a binary search works. 23 | 24 | # The focus of this lesson is unit testing, which is testing the behavior of 25 | # individual methods in isolation. However, every method does not need to be 26 | # tested, so we will look at some basic guidelines that determine whether or not 27 | # a method needs to be tested. 28 | # https://www.artofunittesting.com/definition-of-a-unit-test 29 | 30 | # In general, you probably have 4 different types of methods: 31 | # 1. Command - Changes the observable state, but does not return a value. 32 | # 2. Query - Returns a result, but does not change the observable state. 33 | # 3. Script - Only calls other methods, usually without returning anything. 34 | # 4. Looping Script - Only calls other methods, usually without returning 35 | # anything, and stops when certain conditions are met. 36 | 37 | # Let's take a look at methods that should always be tested: 38 | 39 | # 1. Public Command or Public Query Methods should always be tested, because 40 | # they are the public interface. Command Methods should test the method's action 41 | # or side effect. Query Methods should test the method's return value. 42 | 43 | # 2. Command or Query Methods that are inside a public script or looping script 44 | # method should be tested. For the games that we are making, script and looping 45 | # script methods are just a convenient way to call the methods needed to play a 46 | # full game. Since these methods are required to play the game, they should be 47 | # tested and made public (even if you previously made them private). Pretend 48 | # that someone will be using your class to make their own game with customized 49 | # text. Any method that they would need in their game should be part of the 50 | # public interface and have test coverage. 51 | 52 | # 3. Any method that sends a command message to another class should always test 53 | # that those messages were sent. 54 | 55 | # 4. A Looping Script Method should test the behavior of the method. For 56 | # example, that it stops when certain conditions are met. 57 | 58 | # Here is a summary of what should be tested 59 | # 1. Command Method -> Test the change in the observable state 60 | # 2. Query Method -> Test the return value 61 | # 3. Method with Outgoing Command -> Test that a message is sent 62 | # 4. Looping Script Method -> Test the behavior of the method 63 | 64 | # There are a handful of methods that you do not need to test: 65 | 66 | # 1. You do not have to test #initialize if it is only creating instance 67 | # variables. However, if you call methods inside the initialize method, you 68 | # might need to test #initialize and/or the inside methods. In addition, you 69 | # will need to stub any inside methods because they will be called when you 70 | # create an instance of the class. 71 | 72 | # 2. You do not have to test methods that only contain 'puts' or 'gets' 73 | # because they are well-tested in the standard Ruby library. 74 | 75 | # 3. Private methods do not need to be tested because they should have test 76 | # coverage in public methods. However, as previously discussed, you may have 77 | # some private methods that are called inside a script or looping script method; 78 | # these methods should be tested publicly. 79 | 80 | # Open the file lib/15a_binary_game in a split screen, so you can see both 81 | # files at the same time. We will look at every method and determine if it will 82 | # need to be tested or not. 83 | 84 | describe BinaryGame do 85 | describe '#initialize' do 86 | # Initialize -> No test necessary when only creating instance variables. 87 | end 88 | 89 | describe '#play_game' do 90 | # Public Script Method -> No test necessary, but all methods inside should 91 | # be tested. 92 | end 93 | 94 | describe '#player_input' do 95 | # Located inside #play_game (Public Script Method) 96 | # Looping Script Method -> Test the behavior of the method (for example, it 97 | # stops when certain conditions are met). 98 | 99 | # Note: #player_input will stop looping when the valid_input is between?(min, max) 100 | 101 | subject(:game_input) { described_class.new(1, 10) } 102 | 103 | context 'when user number is between arguments' do 104 | before do 105 | valid_input = '3' 106 | allow(game_input).to receive(:gets).and_return(valid_input) 107 | end 108 | 109 | # When you want to read an instance variable's value that does not need to 110 | # have a reader method, you can use instance_variable_get 111 | # https://www.rubydoc.info/stdlib/core/2.0.0/Object:instance_variable_get 112 | 113 | it 'stops loop and does not display error message' do 114 | min = game_input.instance_variable_get(:@minimum) 115 | max = game_input.instance_variable_get(:@maximum) 116 | error_message = "Input error! Please enter a number between #{min} or #{max}." 117 | expect(game_input).not_to receive(:puts).with(error_message) 118 | game_input.player_input(min, max) 119 | end 120 | end 121 | 122 | # ASSIGNMENT #1 123 | 124 | # Write a test for the following two context blocks. You will need to 125 | # provide 1-2 invalid inputs (letters, symbols, or numbers that are not 126 | # between the min & max integers) and one valid input number (as a string). 127 | 128 | # Remember that a stub can be called multiple times and return different values. 129 | # https://rspec.info/features/3-12/rspec-mocks/configuring-responses/returning-a-value/ 130 | 131 | context 'when user inputs an incorrect value once, then a valid input' do 132 | before do 133 | end 134 | 135 | xit 'completes loop and displays error message once' do 136 | end 137 | end 138 | 139 | context 'when user inputs two incorrect values, then a valid input' do 140 | before do 141 | end 142 | 143 | xit 'completes loop and displays error message twice' do 144 | end 145 | end 146 | end 147 | 148 | # ASSIGNMENT #2 149 | 150 | # Create a new instance of BinaryGame and write a test for the following two 151 | # context blocks. 152 | describe '#verify_input' do 153 | # Located inside #player_input (Looping Script Method) 154 | # Query Method -> Test the return value 155 | 156 | # Note: #verify_input will only return a number if it is between?(min, max) 157 | 158 | context 'when given a valid input as argument' do 159 | xit 'returns valid input' do 160 | end 161 | end 162 | 163 | context 'when given invalid input as argument' do 164 | xit 'returns nil' do 165 | end 166 | end 167 | end 168 | 169 | describe '#update_random_number' do 170 | # Located inside #play_game (Public Script Method) 171 | # Method with Outgoing Command -> Test that a message is sent 172 | 173 | # Look at the #update_random_number in the BinaryGame class. This method 174 | # gets the user's input & wants to update the value of the @random_number. 175 | # If the RandomNumber class had a public setter method (like attr_accessor) 176 | # for @value, we could update @random_number's value inside the BinaryGame 177 | # class. For example: @random_number.value = number_input 178 | 179 | # However, this breaks the encapsulation of the RandomNumber class. As a 180 | # general rule, you want to minimize what classes know about other classes. 181 | # You should not let BinaryGame update the value of a RandomNumber. Instead, 182 | # you want BinaryGame to just send a message to RandomNumber telling it to 183 | # update the value. For example: @random_number.update_value(number_input) 184 | 185 | context 'when updating value of random number' do 186 | # Instead of using a normal double, as we did in TDD, we are going to 187 | # use an instance_double. Differently from the normal test double we've 188 | # been using so far, a verifying double can produce an error if the method 189 | # being stubbed does not exist in the actual class. Verifying doubles are a 190 | # great tool to use when you're doing integration testing and need to make 191 | # sure that different classes work together in order to fulfill some bigger 192 | # computation. 193 | # https://rspec.info/features/3-12/rspec-mocks/verifying-doubles/ 194 | 195 | # You should not use verifying doubles for unit testings. Unit testing relies 196 | # on using doubles to test the object in isolation (i.e., not dependent on any 197 | # other object). One important concept to understand is that the BinarySearch 198 | # or FindNumber class doesn't care if it is given an actual random_number class 199 | # object. It only cares that it is given an object that can respond to certain 200 | # methods. This concept is called polymorphism. 201 | # https://www.geeksforgeeks.org/polymorphism-in-ruby/ 202 | 203 | # Below (commented out) is the previous normal 'random_number' object 204 | # used in TDD. Compare it to the new verifying instance_double for the 205 | # RandomNumber class. 206 | # let(:random_number) { double('random_number', value: 8) } 207 | let(:random_update) { instance_double(RandomNumber, value: 3) } 208 | let(:new_number) { 76 } 209 | subject(:game_update) { described_class.new(1, 100, random_update) } 210 | 211 | # To 'Arrange' this test, two methods will need to be stubbed, so that 212 | # they do not execute. In addition, #player_input will have two arguments 213 | # and will need to return the new_number. We are going to match the 214 | # literal arguments, but there are many ways to specify the arguments 215 | # using matching arguments: 216 | # https://rspec.info/features/3-12/rspec-mocks/setting-constraints/matching-arguments/ 217 | 218 | before do 219 | allow(game_update).to receive(:puts) 220 | allow(game_update).to receive(:player_input).with(1, 100).and_return(new_number) 221 | end 222 | 223 | it 'sends update_value to random_number' do 224 | expect(random_update).to receive(:update_value).with(new_number) 225 | game_update.update_random_number 226 | end 227 | end 228 | end 229 | 230 | describe '#maximum_guesses' do 231 | # Located inside #play_game (Public Script Method) 232 | # Query Method -> Test the return value 233 | 234 | context 'when game minimum and maximum is 1 and 10' do 235 | subject(:game_ten) { described_class.new(1, 10) } 236 | 237 | it 'returns 4' do 238 | max = game_ten.maximum_guesses 239 | expect(max).to eq(4) 240 | end 241 | end 242 | 243 | context 'when game minimum and maximum is 1 and 100' do 244 | subject(:game_hundred) { described_class.new(1, 100) } 245 | 246 | it 'returns 7' do 247 | max = game_hundred.maximum_guesses 248 | expect(max).to eq(7) 249 | end 250 | end 251 | 252 | # ASSIGNMENT #3 253 | 254 | # Write a test for the following context. 255 | context 'when game minimum and maximum is 100 and 600' do 256 | xit 'returns 9' do 257 | end 258 | end 259 | end 260 | 261 | describe '#create_binary_search' do 262 | # Located inside #play_game (Public Script Method) 263 | # Method with Outgoing Command -> Test that a message is sent 264 | 265 | subject(:game_create) { described_class.new(1, 10, number_create) } 266 | let(:number_create) { instance_double(RandomNumber) } 267 | 268 | # Since a new BinarySearch is given a RandomNumber, we can test that it 269 | # receives the correct double. 270 | it 'creates a new BinarySearch with RandomNumber double' do 271 | expect(BinarySearch).to receive(:new).with(1, 10, number_create) 272 | game_create.create_binary_search 273 | end 274 | end 275 | 276 | describe '#display_binary_search' do 277 | # Located inside #play_game (Public Script Method) 278 | 279 | # Looping Script Method -> Test the behavior of the method (for example, it 280 | # stops when certain conditions are met). 281 | 282 | # This method is a Looping Script Method, because #display_turn_order will 283 | # loop until binary_search.game_over? 284 | 285 | subject(:game_display) { described_class.new(1, 10) } 286 | let(:search_display) { instance_double(BinarySearch) } 287 | 288 | context 'when game_over? is false once' do 289 | before do 290 | allow(search_display).to receive(:game_over?).and_return(false, true) 291 | end 292 | 293 | it 'calls display_turn_order one time' do 294 | expect(game_display).to receive(:display_turn_order).with(search_display).once 295 | game_display.display_binary_search(search_display) 296 | end 297 | end 298 | 299 | context 'when game_over? is false twice' do 300 | before do 301 | allow(search_display).to receive(:game_over?).and_return(false, false, true) 302 | end 303 | 304 | it 'calls display_turn_order two times' do 305 | expect(game_display).to receive(:display_turn_order).with(search_display).twice 306 | game_display.display_binary_search(search_display) 307 | end 308 | end 309 | 310 | # ASSIGNMENT #4 311 | 312 | # Write a test for the following context. 313 | context 'when game_over? is false five times' do 314 | xit 'calls display_turn_order five times' do 315 | end 316 | end 317 | end 318 | 319 | # ASSIGNMENT #5 320 | 321 | # Write three tests for the following method. 322 | describe '#display_turn_order' do 323 | # This method is both a Command Method and a Script Method. It changes the 324 | # observable state by incrementing the instance variable @guess_count by one, 325 | # sends two command messages #make_guess and #update_range to the object 326 | # binary_search of class BinarySearch, and sends one command message to `self` 327 | # by calling #display_guess. 328 | 329 | # Create a new subject and an instance_double for BinarySearch. 330 | 331 | before do 332 | # You'll need to create a few method stubs. 333 | end 334 | 335 | # Command Method -> Test the change in the observable state 336 | xit 'increases guess_count by one' do 337 | end 338 | 339 | # Method with Outgoing Command -> Test that a message is sent 340 | xit 'sends make_guess' do 341 | end 342 | 343 | # Method with Outgoing Command -> Test that a message is sent 344 | xit 'sends update_range' do 345 | end 346 | 347 | # Using method expectations can be confusing. Stubbing the methods above 348 | # does not cause this test to pass; it only 'allows' a method to be 349 | # called, if it is called. 350 | 351 | # To test this fact, let's allow a method that is not called in 352 | # #display_turn_order. Uncomment the line at the bottom of this 353 | # paragraph, move it to the before hook, and run the tests. 354 | # All of the tests should continue to pass. Replace 'binary_search_turn' 355 | # with whatever you named your BinarySearch instance double if necessary. 356 | # allow(binary_search_turn).to receive(:game_over?) 357 | 358 | # Now, in the lib/15a_binary_game.rb file, comment out either line, 359 | # binary_search.make_guess or binary_search.update_range. Resave the file 360 | # and rerun the tests. The test for the method that you commented out will 361 | # fail because the method is never called. Now, uncomment the line & resave 362 | # the file to have all tests passing. 363 | end 364 | 365 | describe '#introduction' do 366 | # Located inside #play_game (Public Script Method) 367 | # Only contains puts statements -> No test necessary & can be private. 368 | end 369 | 370 | describe '#display_guess' do 371 | # Located inside #display_binary_search (Looping Script Method) 372 | # Only contains puts statements -> No test necessary & can be private. 373 | end 374 | 375 | describe '#print_number' do 376 | # Only contains puts statements -> No test necessary & can be private. 377 | end 378 | end 379 | -------------------------------------------------------------------------------- /spec/15b_binary_search_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/15b_binary_search' 4 | require_relative '../lib/15c_random_number' 5 | 6 | # The file order to complete this lesson: 7 | 8 | # 1. Familiarize yourself with the three lib/15 files. 9 | # - lib/15a_binary_game 10 | # - lib/15b_binary_search which is based on 14_find_number 11 | # - lib/15c_random_number 12 | # 2. Look at spec/15b_binary_search_spec which is based on 14_find_number_spec 13 | # 3. Complete spec/15a_binary_game_spec 14 | 15 | # This spec file is for the BinarySearch class that is used in the BinaryGame 16 | # class. These tests were written in the last lesson. The FindNumber class 17 | # has been renamed to BinarySearch, which is a more accurate description. 18 | 19 | # This spec file uses verifying doubles to help you get acquaintance with the 20 | # concept. Differently from the normal test double we've been using so far, 21 | # a verifying double can produce an error if the method being stubbed does not 22 | # exist in the actual class. Verifying doubles are a great tool to use when 23 | # you're doing integration testing and need to make sure that different classes 24 | # work together in order to fulfill some bigger computation. 25 | # https://rspec.info/features/3-12/rspec-mocks/verifying-doubles/ 26 | 27 | # You should not use verifying doubles for unit testings. Unit testing relies 28 | # on using doubles to test the object in isolation (i.e., not dependent on any 29 | # other object). One important concept to understand is that the BinarySearch 30 | # or FindNumber class doesn't care if it is given an actual random_number class 31 | # object. It only cares that it is given an object that can respond to certain 32 | # methods. This concept is called polymorphism. 33 | # https://www.geeksforgeeks.org/polymorphism-in-ruby/ 34 | 35 | describe BinarySearch do 36 | describe '#make_guess' do 37 | # Below (commented out) is the previous generic 'random_number' object 38 | # used in TDD. Compare it to the new verifying instance_double for the 39 | # RandomNumber class. 40 | # let(:random_number) { double('random_number', value: 8) } 41 | let(:random_number) { instance_double(RandomNumber, value: 8) } 42 | subject(:game) { described_class.new(0, 9, random_number) } 43 | 44 | context 'when min is 0 and max is 9' do 45 | it 'returns 4' do 46 | guess = game.make_guess 47 | expect(guess).to eq(4) 48 | end 49 | end 50 | 51 | context 'when min is 5 and max is 9' do 52 | subject(:game_five) { described_class.new(5, 9, random_number) } 53 | 54 | it 'returns 7' do 55 | guess = game_five.make_guess 56 | expect(guess).to eq(7) 57 | end 58 | end 59 | 60 | context 'when min is 8 and max is 9' do 61 | subject(:game_eight) { described_class.new(8, 9, random_number) } 62 | 63 | it 'returns 8' do 64 | guess = game_eight.make_guess 65 | expect(guess).to eq(8) 66 | end 67 | end 68 | 69 | context 'when min is 0 and max is 3' do 70 | subject(:game_zero) { described_class.new(0, 3, random_number) } 71 | 72 | it 'returns 1' do 73 | guess = game_zero.make_guess 74 | expect(guess).to eq(1) 75 | end 76 | end 77 | 78 | context 'when min and max both equal 3' do 79 | subject(:game_three) { described_class.new(3, 3, random_number) } 80 | 81 | it 'returns 3' do 82 | guess = game_three.make_guess 83 | expect(guess).to eq(3) 84 | end 85 | end 86 | end 87 | 88 | describe '#game_over?' do 89 | let(:ending_number) { instance_double(RandomNumber, value: 3) } 90 | subject(:ending_game) { described_class.new(0, 9, ending_number, 3) } 91 | 92 | context 'when guess and random_number are equal' do 93 | it 'is game over' do 94 | expect(ending_game).to be_game_over 95 | end 96 | end 97 | 98 | context 'when guess and random_number are not equal' do 99 | let(:mid_number) { instance_double(RandomNumber, value: 5) } 100 | subject(:mid_game) { described_class.new(0, 9, mid_number, 4) } 101 | 102 | it 'is not game over' do 103 | expect(mid_game).to_not be_game_over 104 | end 105 | end 106 | end 107 | 108 | describe '#update_range' do 109 | let(:range_number) { instance_double(RandomNumber, value: 8) } 110 | 111 | context 'when the guess is less than the answer' do 112 | subject(:low_guess_game) { described_class.new(0, 9, range_number, 4) } 113 | 114 | before do 115 | low_guess_game.update_range 116 | end 117 | 118 | it 'updates min' do 119 | minimum = low_guess_game.min 120 | expect(minimum).to eq(5) 121 | end 122 | 123 | it 'does not update max' do 124 | maximum = low_guess_game.max 125 | expect(maximum).to eq(9) 126 | end 127 | end 128 | 129 | context 'when the guess is more than the answer' do 130 | subject(:high_guess_game) { described_class.new(0, 9, range_number, 9) } 131 | 132 | before do 133 | high_guess_game.update_range 134 | end 135 | 136 | it 'does not update min' do 137 | minimum = high_guess_game.min 138 | expect(minimum).to eq(0) 139 | end 140 | 141 | it 'updates max' do 142 | maximum = high_guess_game.max 143 | expect(maximum).to eq(8) 144 | end 145 | end 146 | 147 | context 'when the guess is 7, with min=5 and max=8' do 148 | subject(:eight_game) { described_class.new(5, 8, range_number, 7) } 149 | 150 | before do 151 | eight_game.update_range 152 | end 153 | 154 | it 'updates min to the same value as max' do 155 | minimum = eight_game.min 156 | expect(minimum).to eq(8) 157 | end 158 | 159 | it 'does not update max' do 160 | maximum = eight_game.max 161 | expect(maximum).to eq(8) 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/16a_caesar_breaker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/16a_caesar_breaker' 4 | require_relative '../lib/16b_caesar_translator' 5 | 6 | # The file order to complete this lesson: 7 | 8 | # 1. Familiarize yourself with the four lib/16 files. 9 | # - lib/16_caesar_main 10 | # - lib/16a_caesar_breaker 11 | # - lib/16b_caesar_translator which is based on the typical Caesar Cipher 12 | # - lib/16c_database (module) 13 | # 2. Check out the completed tests in spec/16b_caesar_translator. 14 | # 3. Complete spec/16a_caesar_breaker_spec. 15 | 16 | # The CaesarBreaker class creates the 26 possible translations (using the 17 | # CaesarTranslator class) & saves them in a yaml file (using a Database module). 18 | 19 | # Let's write tests for the CaesarBreaker class & the included Database module. 20 | 21 | # In this lesson, you will be writing tests for 3 methods - 22 | # #create_decrypted_messages, #save_decrypted_messages, and #save_to_yaml. 23 | # In addition, you will learn about testing module methods and how to handle 24 | # testing methods that can raise errors. 25 | 26 | describe CaesarBreaker do 27 | # The tests for CaesarBreaker do not depend on creating different conditions. 28 | # Therefore we can use the same subject instance and CaesarTranslator double. 29 | 30 | subject(:phrase) { described_class.new('Lipps, Asvph!', translator) } 31 | let(:translator) { instance_double(CaesarTranslator) } 32 | 33 | describe '#decrypt' do 34 | # Public Script Method -> No test necessary, but all methods inside should 35 | # be tested. 36 | end 37 | 38 | # ASSIGNMENT #1 39 | 40 | # Write a test for the following method. 41 | 42 | describe '#create_decrypted_messages' do 43 | # This is a Looping Script Method. 44 | # Located inside #decrypt (Public Script Method) 45 | 46 | # Method with Outgoing Command -> Test that a message is sent 47 | xit 'sends translate 26 times' do 48 | end 49 | end 50 | 51 | # MODULE TESTING: There are several ways to test methods inside a module. 52 | # (The following methods are located in lib/16c_database.rb) 53 | 54 | # Some prefer explicitly including it in the configuration option. 55 | # https://rspec.info/features/3-12/rspec-core/helper-methods/modules/ 56 | 57 | # Some prefer testing modules using a dummy class. 58 | # https://mixandgo.com/learn/how-to-test-ruby-modules-with-rspec 59 | 60 | # Modules can also be tested in a class that includes it, which is how the 61 | # following tests work. 62 | 63 | describe '#save_decrypted_messages' do 64 | # Located inside #decrypt (Public Script Method) 65 | 66 | # Method with Outgoing Commands -> Test that the messages are sent 67 | context 'when the directory does not exist' do 68 | # We need to stub several outgoing commands that communicate with 69 | # external systems and create the conditions for this test. In addition, 70 | # there are puts statements in #display_file_location so let's stub that 71 | # method to clean up the test output. 72 | before do 73 | allow(Dir).to receive(:exist?).and_return(false) 74 | allow(Dir).to receive(:mkdir) 75 | allow(File).to receive(:open) 76 | allow(phrase).to receive(:display_file_location) 77 | end 78 | 79 | # ASSIGNMENT #2 80 | # Write the following 3 tests: 81 | 82 | xit 'sends message to check the existence of the 16_cipher directory' do 83 | end 84 | 85 | xit 'sends message to create a directory' do 86 | end 87 | 88 | xit 'sends message to create a file' do 89 | end 90 | end 91 | 92 | # ASSIGNMENT #3 93 | # Write the following 3 tests and the shared before block: 94 | 95 | # Method with Outgoing Commands -> Test that the messages are sent 96 | context 'when the directory exists' do 97 | before do 98 | end 99 | 100 | xit 'sends message to check the existence of the 16_cipher directory' do 101 | end 102 | 103 | xit 'does not send message to create a directory' do 104 | end 105 | 106 | xit 'sends message to create a file' do 107 | end 108 | end 109 | 110 | # This method has a rescue block in case an error occurs. 111 | # Let's test that this method can run without raising an error. 112 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/raise-error/ 113 | 114 | context 'when file is saved successfully' do 115 | before do 116 | allow(Dir).to receive(:exist?).and_return(false) 117 | allow(Dir).to receive(:mkdir) 118 | allow(File).to receive(:open) 119 | allow(phrase).to receive(:display_file_location) 120 | end 121 | 122 | it 'does not raise an error' do 123 | expect { phrase.save_decrypted_messages }.not_to raise_error 124 | end 125 | end 126 | 127 | # Let's simulate an error occurring during #save_decrypted_messages by 128 | # allowing File.open to raise the error 'Errno::ENOENT'. This error means 129 | # that no such file or directory could be found. In addition, when an error 130 | # is rescued there are two puts to stub to clean up the test output. 131 | context 'when rescuing an error' do 132 | before do 133 | allow(Dir).to receive(:exist?).and_return(false) 134 | allow(Dir).to receive(:mkdir) 135 | allow(File).to receive(:open).and_raise(Errno::ENOENT) 136 | allow(phrase).to receive(:puts).twice 137 | end 138 | 139 | # When an error is rescued, the method will not raise an error. Therefore 140 | # you test the conditions that would occur if an error were rescued. 141 | it 'does not display file location' do 142 | expect(phrase).not_to receive(:display_file_location) 143 | phrase.save_decrypted_messages 144 | end 145 | 146 | it 'outputs two error messages' do 147 | expect(phrase).to receive(:puts).with('Error while writing to file LippsAsvph.yaml.') 148 | expect(phrase).to receive(:puts).with(Errno::ENOENT) 149 | phrase.save_decrypted_messages 150 | end 151 | end 152 | end 153 | 154 | # ASSIGNMENT #4 155 | # Write the following test: 156 | 157 | describe '#save_to_yaml' do 158 | # Method with Outgoing Command -> Test that a message is sent 159 | 160 | xit 'dumps to yaml' do 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /spec/16b_caesar_translator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/16b_caesar_translator' 4 | 5 | # The file order to complete this lesson: 6 | 7 | # 1. Familiarize yourself with the four lib/16 files. 8 | # - lib/16_caesar_main 9 | # - lib/16a_caesar_breaker 10 | # - lib/16b_caesar_translator which is based on the typical Caesar Cipher 11 | # - lib/16c_database (module) 12 | # 2. Check out the completed tests in spec/16b_caesar_translator. 13 | # 3. Complete spec/16a_caesar_breaker_spec. 14 | 15 | # This spec file is for the CaesarTranslator class, which is used by the 16 | # CaesarBreaker class. 17 | 18 | describe CaesarTranslator do 19 | # Query Method -> Test the return value 20 | describe '#translate' do 21 | context 'when translating one word' do 22 | subject(:one_word) { described_class.new('Odin') } 23 | 24 | it 'works with small positive shift' do 25 | small_shift = 5 26 | small_result = one_word.translate(small_shift) 27 | expect(small_result).to eql('Tins') 28 | end 29 | 30 | it 'works with small negative shift' do 31 | small_negative_shift = -5 32 | small_negative_result = one_word.translate(small_negative_shift) 33 | expect(small_negative_result).to eql('Jydi') 34 | end 35 | 36 | it 'works with large positive shift' do 37 | large_shift = 74 38 | large_result = one_word.translate(large_shift) 39 | expect(large_result).to eql('Kzej') 40 | end 41 | 42 | it 'works with large negative shift' do 43 | large_negative_shift = -55 44 | large_negative_result = one_word.translate(large_negative_shift) 45 | expect(large_negative_result).to eql('Lafk') 46 | end 47 | end 48 | 49 | context 'when translating a phrase with punctuation' do 50 | subject(:punctuation_phrase) { described_class.new('Hello, World!') } 51 | 52 | it 'works with small positive shift' do 53 | small_shift = 9 54 | small_result = punctuation_phrase.translate(small_shift) 55 | expect(small_result).to eq('Qnuux, Fxaum!') 56 | end 57 | 58 | it 'works with small negative shift' do 59 | small_negative_shift = -5 60 | small_negative_result = punctuation_phrase.translate(small_negative_shift) 61 | expect(small_negative_result).to eql('Czggj, Rjmgy!') 62 | end 63 | 64 | it 'works with large positive shift' do 65 | large_shift = 74 66 | large_result = punctuation_phrase.translate(large_shift) 67 | expect(large_result).to eql('Dahhk, Sknhz!') 68 | end 69 | 70 | it 'works with large negative shift' do 71 | large_negative_shift = -55 72 | large_negative_result = punctuation_phrase.translate(large_negative_shift) 73 | expect(large_negative_result).to eql('Ebiil, Tloia!') 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # This setting enables warnings. It's recommended, but in some cases may 70 | # be too noisy due to issues in dependencies. 71 | config.warnings = true 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /spec_answers/01_string_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe String do 4 | # Create a let variable that will pass the first test. 5 | let(:favorite_food) { String.new('tacos') } 6 | 7 | it 'is equal to tacos' do 8 | expect(favorite_food).to eq('tacos') 9 | end 10 | 11 | context 'when favorite food is updated' do 12 | # Change the favorite_food let variable. 13 | let(:favorite_food) { String.new('fajitas') } 14 | 15 | it 'updates the favorite food' do 16 | # Write a test that will pass. 17 | expect(favorite_food).to eq('fajitas') 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec_answers/02_array_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Array do 4 | context 'when updating an implicit subject' do 5 | it 'is empty' do 6 | # Write a test to expect the subject to be empty. 7 | expect(subject).to be_empty 8 | end 9 | 10 | it 'updates length to 1' do 11 | # Update the implicit subject to make this test past. 12 | subject << 21 13 | expect(subject.length).to eq(1) 14 | end 15 | end 16 | 17 | context 'when using one let variable on two tests' do 18 | # Make a let variable that will pass both tests. 19 | let(:lucky_numbers) { [3, 19, 20] } 20 | 21 | it 'has length of 3' do 22 | expect(lucky_numbers.length).to eq(3) 23 | end 24 | 25 | it 'has sum of 42' do 26 | expect(lucky_numbers.sum).to eq(42) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec_answers/03_number_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Array do 4 | context 'when my_array has perfect values' do 5 | # Write a subject variable 'my_array' that passes all tests. 6 | subject(:my_array) { [-7, 42, 101, 98] } 7 | 8 | it 'has a specific first value' do 9 | expect(my_array.first).to be_odd.and be <= -1 10 | end 11 | 12 | it 'has a specific last value' do 13 | expect(my_array.last).to be_even.and be < 99 14 | end 15 | 16 | it 'has a specific min value' do 17 | expect(my_array.min).not_to be < -9 18 | end 19 | 20 | it 'has a specific max value' do 21 | expect(my_array.max).to be > 100 22 | end 23 | 24 | it 'includes a value of 42' do 25 | expect(my_array).to include(42) 26 | end 27 | 28 | it 'has a fourth element' do 29 | expect(my_array[3]).not_to be_nil 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec_answers/04_hash_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Hash do 4 | subject(:my_car) do 5 | { 6 | make: 'Volkswagen', 7 | model: 'Jetta', 8 | year: 2017, 9 | parking_camera?: true, 10 | assisted_steering?: false 11 | } 12 | end 13 | 14 | it 'is newer than 2015' do 15 | # Write a test that verifies the above statement. 16 | expect(my_car[:year]).to be > 2015 17 | end 18 | 19 | it 'has a parking camera' do 20 | # Write a test that verifies the above statement. 21 | expect(my_car[:parking_camera?]).to be true 22 | end 23 | 24 | it 'does not have assisted steering' do 25 | # Write a test that verifies the above statement. 26 | expect(my_car[:assisted_steering?]).to be false 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec_answers/06_equality_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe 'equality assignment' do 4 | let(:amy) { { fav_color: 'blue', fav_food: 'tacos' } } 5 | let(:bob) { { fav_color: 'blue', fav_food: 'tacos' } } 6 | let(:copy_cat) { amy } 7 | # Write a test that expresses each of the following statements. 8 | 9 | it 'amy is eq to bob' do 10 | expect(amy).to eq(bob) 11 | end 12 | 13 | it 'amy is eql to bob' do 14 | expect(amy).to eql(bob) 15 | end 16 | 17 | it 'amy is not equal to bob' do 18 | expect(amy).not_to be(bob) 19 | end 20 | 21 | it 'copy_cat is equal to amy' do 22 | expect(copy_cat).to be(amy) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec_answers/07_all_contain_exactly_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Array do 4 | subject(:fibonacci_sequence) { [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] } 5 | # Write a test that expresses each of the following statements. 6 | 7 | it 'includes 21 and ends with 89' do 8 | expect(fibonacci_sequence).to include(21).and end_with(89) 9 | end 10 | 11 | it 'starts with 0, 1, 1, 2 and all are under 100' do 12 | expect(fibonacci_sequence).to start_with(0, 1, 1, 2).and all(be < 100) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec_answers/08_change_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe 'lucky numbers with rotate! method' do 4 | subject(:lucky_numbers) { [3, 7, 13, 31, 42] } 5 | # Write a test that expresses each of the following statements. 6 | # Use the array #rotate! method to manipulate lucky_numbers. 7 | 8 | context 'when rotating the array of lucky numbers' do 9 | it 'will change the first value to 7' do 10 | expect { lucky_numbers.rotate! }.to change { lucky_numbers.first }.to(7) 11 | end 12 | 13 | it 'will change the last value to 3' do 14 | expect { lucky_numbers.rotate! }.to change { lucky_numbers.last }.to(3) 15 | end 16 | 17 | it 'will change the first value to 7 and last value to 3' do 18 | expect { lucky_numbers.rotate! }.to change { lucky_numbers.first }.to(7).and change { lucky_numbers.last }.to(3) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec_answers/09_custom_matcher_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # ASSIGNMENT 4 | 5 | describe 'one word palindrome test' do 6 | let(:racecar) { 'racecar' } 7 | let(:spaceship) { 'spaceship' } 8 | let(:rotator) { 'rotator' } 9 | let(:palindrome) { 'palindrome' } 10 | 11 | # Write a custom matcher that detects a one word palindrome, 12 | # using the following block: { |word| word.reverse == word } 13 | # When it is set up correctly, all of the following tests will pass. 14 | 15 | matcher :be_a_palindrome do 16 | match { |word| word.reverse == word } 17 | end 18 | 19 | context 'when a palindrome is used' do 20 | it 'is a palindrome' do 21 | expect(racecar).to be_a_palindrome 22 | end 23 | 24 | it 'is a palindrome' do 25 | expect(rotator).to be_a_palindrome 26 | end 27 | end 28 | 29 | context 'when a palindrome is not used' do 30 | it 'is not a palindrome' do 31 | expect(spaceship).not_to be_a_palindrome 32 | end 33 | 34 | it 'is not a palindrome' do 35 | expect(palindrome).not_to be_a_palindrome 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec_answers/10_drink_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/10_drink' 4 | 5 | describe Drink do 6 | describe '#initialize' do 7 | context 'when type is specified and ounces is default' do 8 | # Create an explicit subject, using 'described_class' and your choice of 9 | # beverage type. 10 | subject(:tea_drink) { described_class.new('tea') } 11 | 12 | it 'is tea' do 13 | expect(tea_drink.type).to eq('tea') 14 | end 15 | 16 | it 'has 16 ounces' do 17 | expect(tea_drink.ounces).to eq(16) 18 | end 19 | end 20 | end 21 | 22 | describe '#full?' do 23 | context 'when drink has 16 ounces or more' do 24 | # Create an explicit subject, using 'described_class' and your choice of 25 | # beverage type. 26 | subject(:default_drink) { described_class.new } 27 | 28 | it 'is full' do 29 | expect(default_drink).to be_full 30 | end 31 | end 32 | 33 | context 'when drink has less than 16 ounces' do 34 | # Create an explicit subject, using 'described_class' and your choice of 35 | # beverage type. In addition, specify ounces to be any number under 16. 36 | subject(:juice_drink) { described_class.new('juice', 8) } 37 | 38 | it 'is not full' do 39 | expect(juice_drink).to_not be_full 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec_answers/11b_cat_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11b_cat' 4 | require_relative '../spec/11a_shared_example_spec' 5 | 6 | describe Cat do 7 | subject(:oscar) { described_class.new('Oscar', 'Maine Coon') } 8 | 9 | # Before you begin this file, make sure you have read the shared 10 | # example file: 11a_shared_examples_spec.rb. This test references 11 | # that file's first test in the below 'include_examples' line. 12 | context 'when Cat is a child class of Pet' do 13 | include_examples 'base class method name' 14 | end 15 | 16 | context 'when cat has name and breed, but no color' do 17 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/have-attributes/ 18 | it 'has name, breed & color attributes' do 19 | expect(oscar).to have_attributes(name: 'Oscar', breed: 'Maine Coon', color: nil) 20 | end 21 | end 22 | end 23 | 24 | # ASSIGNMENT - complete either Cat or Dog assignment 25 | 26 | describe Cat do 27 | # Create a subject with your choice of cat name and optional breed/color. 28 | subject(:lucy) { described_class.new('Lucy', 'British Shorthair') } 29 | 30 | # Write a test using the second shared_example to test that cat responds to 31 | # talk ('meow'). 32 | context 'when Cat has method name shared with other classes' do 33 | include_examples 'shared method name' 34 | end 35 | 36 | it 'is not hungry' do 37 | expect(lucy).to_not be_hungry 38 | end 39 | 40 | it 'is hiding' do 41 | expect(lucy).to be_hiding 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec_answers/11c_dog_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/11c_dog' 4 | require_relative '../spec/11a_shared_example_spec' 5 | 6 | describe Dog do 7 | subject(:toby) { described_class.new('Toby', nil, 'brown') } 8 | 9 | # Before you begin this file, make sure you have read the shared 10 | # example file: 11a_shared_examples_spec.rb. This test references 11 | # that file's first test in the below 'include_examples' line. 12 | context 'when Dog is a child class of Pet' do 13 | include_examples 'base class method name' 14 | end 15 | 16 | context 'when dog has name and color, but no breed' do 17 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/have-attributes/ 18 | it 'has name, breed & color attributes' do 19 | expect(toby).to have_attributes(name: 'Toby', breed: nil, color: 'brown') 20 | end 21 | end 22 | end 23 | 24 | describe Dog do 25 | # Create a subject with your choice of cat name and optional breed/color. 26 | subject(:ollie) { described_class.new('Ollie', nil, 'white') } 27 | 28 | # Write a test using the second shared_example to test that dog responds to 29 | # talk ('WOOF!'). 30 | context 'when Dog has method name shared with other classes' do 31 | include_examples 'shared method name' 32 | end 33 | 34 | it 'is not barking' do 35 | expect(ollie).to_not be_barking 36 | end 37 | 38 | it 'is sleeping' do 39 | expect(ollie).to be_sleeping 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec_answers/12_magic_seven_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/12_magic_seven' 4 | 5 | # The file order to complete this lesson: 6 | # 1. Familiarize yourself with the class in lib/12_magic_seven.rb 7 | # 2. Complete spec/12_magic_seven_spec.rb 8 | 9 | # Before learning any more complexities of testing, let's take a look at a 10 | # standard testing pattern: Arrange, Act, and Assert. 11 | # https://youtu.be/sCthIEOaMI8 12 | 13 | # 1. Arrange -> set up the test (examples: initializing objects, let 14 | # variables, updating values of instance variables). 15 | # 2. Act -> execute the logic to test (example: calling a method to run). 16 | # 3. Assert -> expect the results of arrange & act. 17 | 18 | # The tests in this lesson are fairly easy to understand, and it may seem 19 | # ridiculous to use A-A-A for them. However, tests should be easily understood 20 | # not just by you, but also by someone that is not familiar with the code. 21 | 22 | # NOTE: When you start using A-A-A to format your tests, it will feel 23 | # strange to not be following DRY (Don't Repeat Yourself). With tests, however, 24 | # repetition is necessary in order for them to be easy to read. If you are using 25 | # rubocop, you can disable specific (or all) cops for certain files (or 26 | # directories) by adding a .rubocop.yml file. 27 | # https://docs.rubocop.org/rubocop/0.88/configuration.html#includingexcluding-files 28 | 29 | # When you start working on a existing code base, you will often become familiar 30 | # with the code by reading the tests. 31 | 32 | describe MagicSeven do 33 | # This next line should be very familiar, and it is part of the 'Arrange' step. 34 | subject(:game) { described_class.new } 35 | 36 | describe '#add_nine' do 37 | # This test could be written as below (and it would pass): 38 | it 'returns 15' do 39 | expect(game.add_nine(6)).to eq(15) 40 | end 41 | 42 | # However, the above test is NOT very readable. For example, it does not 43 | # explain where '6' came from. So let's start with explaining 44 | # where '6' came from, as part of the 'Arrange' step. 45 | it 'returns 15' do 46 | random_number = 6 47 | 48 | # For the 'Act' step, we will be testing the result of the logic of adding 49 | # nine to the random_number. 50 | result = game.add_nine(random_number) 51 | 52 | # For the 'Assert' step, we know exactly we expect the result to be: 53 | expect(result).to eq(15) 54 | end 55 | end 56 | 57 | # In addition, using a context to explain the conditions of the test makes 58 | # the output more readable. 59 | describe '#multiply_by_two' do 60 | context 'when the previous step is 8' do 61 | it 'returns 16' do 62 | previous_step = 8 # Arrange 63 | result = game.multiply_by_two(previous_step) # Act 64 | expect(result).to eq(16) # Assert 65 | end 66 | end 67 | end 68 | 69 | # ASSIGNMENT 70 | # Write a test for each of the following methods: 71 | 72 | describe '#subtract_four' do 73 | context 'when the previous step is 10' do 74 | it 'returns 6' do 75 | previous_step = 10 76 | result = game.subtract_four(previous_step) 77 | expect(result).to eq(6) 78 | end 79 | end 80 | end 81 | 82 | describe '#divide_by_two' do 83 | context 'when the previous step is 18' do 84 | it 'returns 9' do 85 | previous_step = 18 86 | result = game.divide_by_two(previous_step) 87 | expect(result).to eq(9) 88 | end 89 | end 90 | end 91 | 92 | # The following tests will need you to create new instances of MagicSeven with 93 | # a specific value for the random_number. 94 | describe '#subtract_random_number' do 95 | context 'when the previous step is 10 and random_number is 3' do 96 | subject(:game_three) { described_class.new(3) } 97 | 98 | it 'returns 7' do 99 | previous_step = 10 100 | result = game_three.subtract_random_number(previous_step) 101 | expect(result).to eq(7) 102 | end 103 | end 104 | end 105 | 106 | # The #play method will always return seven! Test this game, using any 107 | # integer as the random_number. Update the context with the number. 108 | describe '#play' do 109 | context 'when the random number is 19' do 110 | subject(:game_nineteen) { described_class.new(19) } 111 | 112 | it 'will always return 7' do 113 | result = game_nineteen.play 114 | expect(result).to eq(7) 115 | end 116 | end 117 | 118 | context 'when the random number is 1001' do 119 | subject(:game_thousand) { described_class.new(1001) } 120 | 121 | it 'will always return 7' do 122 | result = game_thousand.play 123 | expect(result).to eq(7) 124 | end 125 | end 126 | 127 | context 'when the random number is 987,654,321' do 128 | subject(:game_huge) { described_class.new(987_654_321) } 129 | 130 | it 'will always return 7' do 131 | result = game_huge.play 132 | expect(result).to eq(7) 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec_answers/13_input_output_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/13_input_output' 4 | 5 | # The file order to complete this lesson: 6 | # 1. Familiarize yourself with the two classes in lib/13_input_output.rb 7 | # 2. Complete spec/13_input_output_spec.rb 8 | 9 | # Ruby code that was written before you learned how to use RSpec may be nearly 10 | # impossible to test. For example, in the lib/13_input_output file, there are 11 | # two identical games: ImpossibleToTestGame and NumberGame. Take a look at each 12 | # game and look for differences that may make one easier or harder to test 13 | # than the other. 14 | 15 | # One key difference between the two is that NumberGame has smaller, 16 | # isolated methods. 17 | 18 | # Small and isolated methods that only do one thing are easier to test. 19 | # Long methods are like a run-on sentence that should have been divided into 2 or 3 different sentences so that everything could be clearly understood and in this case if a method does many different things it can be difficult to test. 20 | 21 | # So if you are new to testing, be open to refactoring your previous 22 | # code, to make writing tests easier. As you learn testing, you will also 23 | # learn how to write better testable methods. 24 | 25 | # The tests in this file are longer than those in the previous lessons. 26 | # To get your bearings, remember to reference the following lines: 27 | # describe -> Name of the method that is being tested. 28 | # context -> Explains the conditions of the test. 29 | # it -> Explains the results of the test. 30 | 31 | describe NumberGame do 32 | subject(:game) { described_class.new } 33 | 34 | describe '#initialize' do 35 | context 'when solution is initialized' do 36 | it 'is a number greater than or equal to 0' do 37 | solution = game.solution 38 | expect(solution).to be >= 0 39 | end 40 | 41 | it 'is a number less than or equal to 9' do 42 | solution = game.solution 43 | expect(solution).to be <= 9 44 | end 45 | 46 | # ASSIGNMENT #1 47 | 48 | # Write a similar test to the one above, that uses a custom matcher 49 | # instead of <, >, =. 50 | matcher :be_between_zero_and_nine do 51 | match { |number| number.between?(0, 9) } 52 | end 53 | 54 | it 'is a number between 0 and 9' do 55 | solution = game.solution 56 | expect(solution).to be_between_zero_and_nine 57 | end 58 | end 59 | end 60 | 61 | describe '#game_over?' do 62 | context 'when user guess is correct' do 63 | # To test this method, we need to set specific values for @solution and 64 | # @guess, so we will create a new instance of NumberGame. When creating 65 | # another instance of NumberGame, we'll use a meaningful name to 66 | # differentiate between instances. 67 | 68 | # A helpful tip is to combine the purpose of the test and the object. 69 | # E.g., game_end or complete_game. 70 | 71 | subject(:game_end) { described_class.new(5, '5') } 72 | 73 | it 'is game over' do 74 | expect(game_end).to be_game_over 75 | end 76 | end 77 | 78 | # ASSIGNMENT #2 79 | 80 | # Create a new instance of NumberGame and write a test for when the @guess 81 | # does not equal @solution. 82 | context 'when user guess is not correct' do 83 | subject(:game_mid) { described_class.new(5, '2') } 84 | 85 | it 'is not game over' do 86 | expect(game_mid).to_not be_game_over 87 | end 88 | end 89 | end 90 | 91 | # The #player_input method is used in the game as an argument passed into the 92 | # verify_input method. The #player_input method is not tested because it is a 93 | # private method. In addition, it is unnecessary to test methods that only 94 | # contain puts and/or gets. 95 | 96 | # Since we do not have to test #player_input, let's test #verify_input. 97 | 98 | describe '#verify_input' do 99 | subject(:game_check) { described_class.new } 100 | # NOTE: #verify_input will only return a value if it matches /^[0-9]$/ 101 | 102 | context 'when given a valid input as argument' do 103 | it 'returns valid input' do 104 | user_input = '3' 105 | verified_input = game_check.verify_input(user_input) 106 | expect(verified_input).to eq('3') 107 | end 108 | end 109 | 110 | # ASSIGNMENT #3 111 | 112 | # Write a test for the following context. 113 | context 'when given invalid input as argument' do 114 | it 'returns nil' do 115 | letter_input = 'g' 116 | verified_input = game_check.verify_input(letter_input) 117 | expect(verified_input).to be_nil 118 | end 119 | end 120 | end 121 | 122 | describe '#player_turn' do 123 | # In order to test the behavior of #player_turn, we need to use a method 124 | # stub for #player_input to return a valid_input ('3'). To stub a method, 125 | # we 'allow' the test subject (game_loop) to receive the :method_name 126 | # and to return a specific value. 127 | # https://rspec.info/features/3-12/rspec-mocks/basics/allowing-messages/ 128 | # http://testing-for-beginners.rubymonstas.org/test_doubles.html 129 | # https://edpackard.medium.com/the-no-problemo-basic-guide-to-doubles-and-stubs-in-rspec-1af3e13b158 130 | 131 | subject(:game_loop) { described_class.new } 132 | 133 | context 'when user input is valid' do 134 | # To test the behavior, we want to test that the loop stops before the 135 | # puts 'Input error!' line. In order to test that this method is not 136 | # called, we use a message expectation. 137 | # https://rspec.info/features/3-12/rspec-mocks/ 138 | 139 | it 'stops loop and does not display error message' do 140 | valid_input = '3' 141 | allow(game_loop).to receive(:player_input).and_return(valid_input) 142 | # To use a message expectation, move 'Assert' before 'Act'. 143 | expect(game_loop).not_to receive(:puts).with('Input error!') 144 | game_loop.player_turn 145 | end 146 | end 147 | 148 | context 'when user inputs an incorrect value once, then a valid input' do 149 | # A method stub can be called multiple times and return different values. 150 | # https://rspec.info/features/3-12/rspec-mocks/configuring-responses/returning-a-value/ 151 | # Create a stub method to receive :player_input and return the invalid 152 | # 'letter' input, then the 'valid_input' 153 | 154 | # As the 'Arrange' step for tests grows, you can use a before hook to 155 | # separate the test from the set-up. 156 | # https://rspec.info/features/3-12/rspec-core/hooks/before-and-after-hooks/ 157 | # https://www.tutorialspoint.com/rspec/rspec_hooks.htm 158 | before do 159 | letter = 'd' 160 | valid_input = '8' 161 | allow(game_loop).to receive(:player_input).and_return(letter, valid_input) 162 | end 163 | 164 | # When using message expectations, you can specify how many times you 165 | # expect the message to be received. 166 | # https://rspec.info/features/3-12/rspec-mocks/setting-constraints/receive-counts/ 167 | it 'completes loop and displays error message once' do 168 | expect(game_loop).to receive(:puts).with('Input error!').once 169 | game_loop.player_turn 170 | end 171 | end 172 | 173 | # ASSIGNMENT #4 174 | 175 | # Write a test for the following context. 176 | context 'when user inputs two incorrect values, then a valid input' do 177 | before do 178 | letter = 'd' 179 | symbol = '$' 180 | valid_input = '2' 181 | allow(game_loop).to receive(:player_input).and_return(letter, symbol, valid_input) 182 | end 183 | 184 | it 'completes loop and displays error message twice' do 185 | expect(game_loop).to receive(:puts).with('Input error!').twice 186 | game_loop.player_turn 187 | end 188 | end 189 | end 190 | 191 | # It is unneccessary to write tests for methods that only contain puts 192 | # statements, like #final_message. Puts is a basic part of the standard 193 | # ruby library & is already well tested. Plus, most 'real world 194 | # applications' don't even output like this except to loggers. 195 | 196 | # However, here is an example of how to test 'puts' using the output matcher. 197 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/output/ 198 | 199 | describe '#final_message' do 200 | context 'when count is 1' do 201 | # To test this method, we need to set specific values for @solution, 202 | # @guess and @count, so we will create a new instance of NumberGame. 203 | subject(:game_one) { described_class.new(5, '5', 1) } 204 | 205 | it 'outputs correct phrase' do 206 | lucky_phrase = "LUCKY GUESS!\n" 207 | # The output matcher needs a block of code to assert. 208 | expect { game_one.final_message }.to output(lucky_phrase).to_stdout 209 | end 210 | end 211 | 212 | # ASSIGNMENT #5 213 | 214 | # Create a new instance of NumberGame, with specific values for @solution, 215 | # @guess, and @count 216 | context 'when count is 2-3' do 217 | subject(:game_three) { described_class.new(5, '5', 3) } 218 | 219 | it 'outputs correct phrase' do 220 | congrats_phrase = "Congratulations! You picked the random number in 3 guesses!\n" 221 | expect { game_three.final_message }.to output(congrats_phrase).to_stdout 222 | end 223 | end 224 | 225 | # ASSIGNMENT #6 226 | 227 | # Write a test for the following context. 228 | context 'when count is 4 and over' do 229 | subject(:game_seven) { described_class.new(5, '5', 7) } 230 | 231 | it 'outputs correct phrase' do 232 | hard_phrase = "That was hard. It took you 7 guesses!\n" 233 | expect { game_seven.final_message }.to output(hard_phrase).to_stdout 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /spec_answers/14_find_number_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # All answers for this TDD example are in this file 4 | # (FindNumber class and rspec tests ) 5 | 6 | # class for computer to find random number 7 | class FindNumber 8 | attr_reader :min, :max, :answer, :guess 9 | 10 | def initialize(min, max, answer = RandomNumber.new(min, max), guess = nil) 11 | @min = min 12 | @max = max 13 | @answer = answer.value 14 | @guess = guess 15 | end 16 | 17 | def make_guess 18 | @guess = (min + max) / 2 19 | end 20 | 21 | def game_over? 22 | guess == answer 23 | end 24 | 25 | def update_range 26 | guess < answer ? @min = guess + 1 : @max = guess - 1 27 | end 28 | end 29 | 30 | # ASSIGNMENT 31 | # For this lesson you will be doing TDD for 3 methods: #make_guess, 32 | # #game_over?, and #update_range. 33 | 34 | # After you have some experience using TDD, you can use the typical 35 | # Red-Green-Refactor workflow. 36 | # https://thoughtbot.com/upcase/videos/red-green-refactor-by-example 37 | 38 | # Since this is probably your first experience with TDD, let's extend the 39 | # workflow to include a few more steps: 40 | # 1. Read & understand the requirement for one method only. 41 | # 2. Write one test for that method; run the tests to see it fail. 42 | # 3. Write the method to fulfill the requirement. 43 | # 4. Run the tests again. If they don't all pass, redo steps 1-3. 44 | # 5. When your first test is passing, write the additional tests. 45 | # 6. Run all of the tests. If any do not pass, redo steps 3-5. 46 | # 7. Optional: Refactor your code and/or tests, keeping all tests passing. 47 | 48 | describe FindNumber do 49 | # ASSIGNMENT: METHOD #1 50 | 51 | # The basic idea of 'FindNumber' is to program a computer to guess a 52 | # random_number using binary search. Remember the binary search video 53 | # that you watched in the Computer Science section: 54 | # https://www.youtube.com/watch?v=T98PIp4omUA 55 | 56 | # The computer will update min and max values to help find the correct number. 57 | 58 | describe '#make_guess' do 59 | # Create a random_number double called 'number_guessing'. Allow the double 60 | # to receive 'value' and return the value of 8, in one of the two ways 61 | # explained above. 62 | 63 | let(:number_guessing) { double('random_number', value: 8) } 64 | subject(:game_guessing) { described_class.new(0, 9, number_guessing) } 65 | 66 | # Before you write the #make_guess method: 67 | # Write a test that would expect #make_guess to return the average of 68 | # the min and max values (rounded down). Don't expect this test to be 69 | # able to pass as you haven't written #make_guess yet! 70 | context 'when min is 0 and max is 9' do 71 | it 'returns 4' do 72 | guess = game_guessing.make_guess 73 | expect(guess).to eq(4) 74 | end 75 | end 76 | 77 | # Now write a method in 14_find_number.rb called #make_guess that returns 78 | # the average of the min and max values (rounded down). 79 | # You can now run the above test and it should pass. 80 | 81 | # Write a test for each of the following contexts. You will need to create a 82 | # new instance of FindNumber for each context, but you can use the same 83 | # random number double created inside this method's describe block. 84 | 85 | context 'when min is 5 and max is 9' do 86 | subject(:game_five) { described_class.new(5, 9, number_guessing) } 87 | 88 | it 'returns 7' do 89 | guess = game_five.make_guess 90 | expect(guess).to eq(7) 91 | end 92 | end 93 | 94 | context 'when min is 8 and max is 9' do 95 | subject(:game_eight) { described_class.new(8, 9, number_guessing) } 96 | 97 | it 'returns 8' do 98 | guess = game_eight.make_guess 99 | expect(guess).to eq(8) 100 | end 101 | end 102 | 103 | context 'when min is 0 and max is 3' do 104 | subject(:game_zero) { described_class.new(0, 3, number_guessing) } 105 | 106 | it 'returns 1' do 107 | guess = game_zero.make_guess 108 | expect(guess).to eq(1) 109 | end 110 | end 111 | 112 | context 'when min and max both equal 3' do 113 | subject(:game_three) { described_class.new(3, 3, number_guessing) } 114 | 115 | it 'returns 3' do 116 | guess = game_three.make_guess 117 | expect(guess).to eq(3) 118 | end 119 | end 120 | end 121 | 122 | # ASSIGNMENT: METHOD #2 123 | describe '#game_over?' do 124 | context 'when guess and random_number are equal' do 125 | # Create another subject and random_number double with meaningful names. 126 | # The subject will need to specify the number value of @guess. 127 | 128 | # Allow the double to receive 'value' and return the same number as @guess. 129 | 130 | # Write a test that would expect game to be_game_over when a guess equals 131 | # the random_number double's value above. Remember that this test will not 132 | # be able to pass yet because you haven't written the method! 133 | 134 | let(:number_end) { double('random_number', value: 3) } 135 | subject(:game_end) { described_class.new(0, 9, number_end, 3) } 136 | 137 | it 'is game over' do 138 | expect(game_end).to be_game_over 139 | end 140 | end 141 | 142 | # Write the corresponding method in 14_find_number.rb called #game_over? 143 | # that returns true when a guess equals the value of the random_number. 144 | 145 | # Write a test that would expect game to NOT be_game_over when a guess does 146 | # NOT equal the random_number double's value above. 147 | 148 | context 'when guess and random_number are not equal' do 149 | let(:number_mid) { double('random_number', value: 5) } 150 | subject(:game_mid) { described_class.new(0, 9, number_mid, 4) } 151 | 152 | it 'is not game over' do 153 | expect(game_mid).to_not be_game_over 154 | end 155 | end 156 | end 157 | 158 | # ASSIGNMENT: METHOD #3 159 | describe '#update_range' do 160 | # As you have seen above, you can share the same random_number double for 161 | # multiple context blocks, by declaring it inside the describe block. 162 | let(:number_range) { double('random_number', value: 8) } 163 | 164 | # Write four tests for #update_range that test the values of min and max. 165 | 166 | # When the guess is less than the answer: 167 | # - min updates to one more than the guess 168 | # - max stays the same 169 | 170 | # When the guess is more than the answer: 171 | # - min stays the same 172 | # - max updates to one less than the guess 173 | 174 | context 'when the guess is less than the answer' do 175 | subject(:low_guess_game) { described_class.new(0, 9, number_range, 4) } 176 | 177 | before do 178 | low_guess_game.update_range 179 | end 180 | 181 | it 'updates min to 5' do 182 | minimum = low_guess_game.min 183 | expect(minimum).to eq(5) 184 | end 185 | 186 | it 'does not update max' do 187 | maximum = low_guess_game.max 188 | expect(maximum).to eq(9) 189 | end 190 | end 191 | 192 | context 'when the guess is more than the answer' do 193 | subject(:high_guess_game) { described_class.new(0, 9, number_range, 9) } 194 | 195 | before do 196 | high_guess_game.update_range 197 | end 198 | 199 | it 'does not update min' do 200 | minimum = high_guess_game.min 201 | expect(minimum).to eq(0) 202 | end 203 | 204 | it 'updates max to 8' do 205 | maximum = high_guess_game.max 206 | expect(maximum).to eq(8) 207 | end 208 | end 209 | 210 | # Now, write the method in 14_find_number.rb called #update_range that will 211 | # do the following: 212 | 213 | # When the guess is less than the answer: 214 | # - min updates to one more than the guess 215 | 216 | # When the guess is not less than the answer: 217 | # - max updates to one less than the guess 218 | 219 | # Write a test for any 'edge cases' that you can think of, for example: 220 | 221 | context 'when the guess is 7, with min=5 and max=8' do 222 | subject(:game_range) { described_class.new(5, 8, number_range, 7) } 223 | 224 | before do 225 | game_range.update_range 226 | end 227 | 228 | it 'updates min to the same value as max' do 229 | minimum = game_range.min 230 | expect(minimum).to eq(8) 231 | end 232 | 233 | it 'does not update max' do 234 | maximum = game_range.max 235 | expect(maximum).to eq(8) 236 | end 237 | end 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /spec_answers/15a_binary_game_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/15a_binary_game' 4 | require_relative '../lib/15b_binary_search' 5 | require_relative '../lib/15c_random_number' 6 | 7 | # The file order to complete this lesson: 8 | 9 | # 1. Familiarize yourself with the four lib/15 files. 10 | # - lib/15_binary_main 11 | # - lib/15a_binary_game 12 | # - lib/15b_binary_search which is based on 14_find_number 13 | # - lib/15c_random_number 14 | # 2. Read the comments in spec/15b_binary_search_spec 15 | # 3. Complete spec/15a_binary_game_spec 16 | 17 | # As noted above, the files for this lesson (#15) build on the TDD files 18 | # from #14. The FindNumber class is now called BinarySearch, which is a more 19 | # accurate description. 20 | 21 | # This lesson has a new class called BinaryGame. This BinaryGame class uses 22 | # the BinarySearch class and visualizes how a binary search works. 23 | 24 | # The focus of this lesson is unit testing, which is testing the behavior of 25 | # individual methods in isolation. However, every method does not need to be 26 | # tested, so we will look at some basic guidelines that determine whether or not 27 | # a method needs to be tested. 28 | # https://www.artofunittesting.com/definition-of-a-unit-test 29 | 30 | # In general, you probably have 4 different types of methods: 31 | # 1. Command - Changes the observable state, but does not return a value. 32 | # 2. Query - Returns a result, but does not change the observable state. 33 | # 3. Script - Only calls other methods, usually without returning anything. 34 | # 4. Looping Script - Only calls other methods, usually without returning 35 | # anything, and stops when certain conditions are met. 36 | 37 | # Let's take a look at methods that should always be tested: 38 | 39 | # 1. Public Command or Public Query Methods should always be tested, because 40 | # they are the public interface. Command Methods should test the method's action 41 | # or side effect. Query Methods should test the method's return value. 42 | 43 | # 2. Command or Query Methods that are inside a public script or looping script 44 | # method should be tested. For the games that we are making, script and looping 45 | # script methods are just a convenient way to call the methods needed to play a 46 | # full game. Since these methods are required to play the game, they should be 47 | # tested and made public (even if you previously made them private). Pretend 48 | # that someone will be using your class to make their own game with customized 49 | # text. Any method that they would need in their game should be part of the 50 | # public interface and have test coverage. 51 | 52 | # 3. Any method that sends a command message to another class should always test 53 | # that those messages were sent. 54 | 55 | # 4. A Looping Script Method should test the behavior of the method. For 56 | # example, that it stops when certain conditions are met. 57 | 58 | # Here is a summary of what should be tested 59 | # 1. Command Method -> Test the change in the observable state 60 | # 2. Query Method -> Test the return value 61 | # 3. Method with Outgoing Command -> Test that a message is sent 62 | # 4. Looping Script Method -> Test the behavior of the method 63 | 64 | # There are a handful of methods that you do not need to test: 65 | 66 | # 1. You do not have to test #initialize if it is only creating instance 67 | # variables. However, if you call methods inside the initialize method, you 68 | # might need to test #initialize and/or the inside methods. In addition, you 69 | # will need to stub any inside methods because they will be called when you 70 | # create an instance of the class. 71 | 72 | # 2. You do not have to test methods that only contain 'puts' or 'gets' 73 | # because they are well-tested in the standard Ruby library. 74 | 75 | # 3. Private methods do not need to be tested because they should have test 76 | # coverage in public methods. However, as previously discussed, you may have 77 | # some private methods that are called inside a script or looping script method; 78 | # these methods should be tested publicly. 79 | 80 | # Open the file lib/15a_binary_game in a split screen, so you can see both 81 | # files at the same time. We will look at every method and determine if it will 82 | # need to be tested or not. 83 | 84 | describe BinaryGame do 85 | describe '#initialize' do 86 | # Initialize -> No test necessary, when only creating instance variables. 87 | end 88 | 89 | describe '#play_game' do 90 | # Public Script Method -> No test necessary, but all methods inside should 91 | # be tested. 92 | end 93 | 94 | describe '#player_input' do 95 | # Located inside #play_game (Public Script Method) 96 | # Looping Script Method -> Test the behavior of the method (for example, it 97 | # stops when certain conditions are met). 98 | 99 | # NOTE: #player_input will stop looping when the valid_input is between?(min, max) 100 | 101 | subject(:game_input) { described_class.new(1, 10) } 102 | 103 | context 'when user number is between arguments' do 104 | before do 105 | valid_input = '3' 106 | allow(game_input).to receive(:gets).and_return(valid_input) 107 | end 108 | 109 | # When you want to read an instance variable's value that does not need to 110 | # have a reader method, you can use instance_variable_get 111 | # https://www.rubydoc.info/stdlib/core/2.0.0/Object:instance_variable_get 112 | 113 | it 'stops loop and does not display error message' do 114 | min = game_input.instance_variable_get(:@minimum) 115 | max = game_input.instance_variable_get(:@maximum) 116 | error_message = "Input error! Please enter a number between #{min} or #{max}." 117 | expect(game_input).not_to receive(:puts).with(error_message) 118 | game_input.player_input(min, max) 119 | end 120 | end 121 | 122 | # ASSIGNMENT #1 123 | 124 | # Write a test for the following two context blocks. You will need to 125 | # provide 1-2 invalid inputs (letters, symbols, or numbers that are not 126 | # between the min & max integers) and one valid input number (as a string). 127 | 128 | # Remember that a stub can be called multiple times and return different values. 129 | # https://rspec.info/features/3-12/rspec-mocks/configuring-responses/returning-a-value/ 130 | 131 | context 'when user inputs an incorrect value once, then a valid input' do 132 | before do 133 | letter = 'q' 134 | valid_input = '3' 135 | allow(game_input).to receive(:gets).and_return(letter, valid_input) 136 | end 137 | 138 | it 'completes loop and displays error message once' do 139 | min = game_input.instance_variable_get(:@minimum) 140 | max = game_input.instance_variable_get(:@maximum) 141 | error_message = "Input error! Please enter a number between #{min} or #{max}." 142 | expect(game_input).to receive(:puts).with(error_message).once 143 | game_input.player_input(min, max) 144 | end 145 | end 146 | 147 | context 'when user inputs two incorrect values, then a valid input' do 148 | before do 149 | letter = 'q' 150 | symbol = '@' 151 | valid_input = '3' 152 | allow(game_input).to receive(:gets).and_return(letter, symbol, valid_input) 153 | end 154 | 155 | it 'completes loop and displays error message twice' do 156 | min = game_input.instance_variable_get(:@minimum) 157 | max = game_input.instance_variable_get(:@maximum) 158 | error_message = "Input error! Please enter a number between #{min} or #{max}." 159 | expect(game_input).to receive(:puts).with(error_message).twice 160 | game_input.player_input(min, max) 161 | end 162 | end 163 | end 164 | 165 | # ASSIGNMENT #2 166 | 167 | # Create a new instance of BinaryGame and write a test for the following two 168 | # context blocks. 169 | describe '#verify_input' do 170 | # Located inside #player_input (Looping Script Method) 171 | # Query Method -> Test the return value 172 | 173 | # NOTE: #verify_input will only return a number if it is between?(min, max) 174 | 175 | subject(:verify_game) { described_class.new(1, 100) } 176 | 177 | context 'when given a valid input as argument' do 178 | it 'returns valid input' do 179 | min = verify_game.instance_variable_get(:@minimum) 180 | max = verify_game.instance_variable_get(:@maximum) 181 | user_number = 48 182 | verified_input = verify_game.verify_input(min, max, user_number) 183 | expect(verified_input).to eq(48) 184 | end 185 | end 186 | 187 | context 'when given invalid input as argument' do 188 | it 'returns nil' do 189 | min = verify_game.instance_variable_get(:@minimum) 190 | max = verify_game.instance_variable_get(:@maximum) 191 | user_number = 234 192 | verified_input = verify_game.verify_input(min, max, user_number) 193 | expect(verified_input).to be_nil 194 | end 195 | end 196 | end 197 | 198 | describe '#update_random_number' do 199 | # Located inside #play_game (Public Script Method) 200 | # Method with Outgoing Command -> Test that a message is sent 201 | 202 | # Look at the #update_random_number in the BinaryGame class. This method 203 | # gets the user's input & wants to update the value of the @random_number. 204 | # If the RandomNumber class had a public setter method (like attr_accessor) 205 | # for @value, we could update @random_number's value inside the BinaryGame 206 | # class. For example: @random_number.value = number_input 207 | 208 | # However, this breaks the encapsulation of the RandomNumber class. As a 209 | # general rule, you want to minimize what classes know about other classes. 210 | # You should not let BinaryGame update the value of a RandomNumber. Instead, 211 | # you want BinaryGame to just send a message to RandomNumber telling it to 212 | # update the value. For example: @random_number.update_value(number_input) 213 | 214 | context 'when updating value of random number' do 215 | # Instead of using a normal double, as we did in TDD, we are going to 216 | # use an instance_double. Differently from the normal test double we've 217 | # been using so far, a verifying double can produce an error if the method 218 | # being stubbed does not exist in the actual class. Verifying doubles are a 219 | # great tool to use when you're doing integration testing and need to make 220 | # sure that different classes work together in order to fulfill some bigger 221 | # computation. 222 | # https://rspec.info/features/3-12/rspec-mocks/verifying-doubles/ 223 | 224 | # You should not use verifying doubles for unit testings. Unit testing relies 225 | # on using doubles to test the object in isolation (i.e., not dependent on any 226 | # other object). One important concept to understand is that the BinarySearch 227 | # or FindNumber class doesn't care if it is given an actual random_number class 228 | # object. It only cares that it is given an object that can respond to certain 229 | # methods. This concept is called polymorphism. 230 | # https://www.geeksforgeeks.org/polymorphism-in-ruby/ 231 | 232 | # Below (commented out) is the previous normal 'random_number' object 233 | # used in TDD. Compare it to the new verifying instance_double for the 234 | # RandomNumber class. 235 | # let(:random_number) { double('random_number', value: 8) } 236 | let(:random_update) { instance_double(RandomNumber) } 237 | let(:new_number) { 76 } 238 | subject(:game_update) { described_class.new(1, 100, random_update) } 239 | 240 | # To 'Arrange' this test, two methods will need to be stubbed, so that 241 | # they do not execute. In addition, #player_input will have two arguments 242 | # and will need to return the new_number. We are going to match the 243 | # literal arguments, but there are many ways to specify the arguments 244 | # using matching arguments: 245 | # https://rspec.info/features/3-12/rspec-mocks/setting-constraints/matching-arguments/ 246 | 247 | before do 248 | allow(game_update).to receive(:puts) 249 | allow(game_update).to receive(:player_input).with(1, 100).and_return(new_number) 250 | end 251 | 252 | it 'sends update_value to random_number' do 253 | expect(random_update).to receive(:update_value).with(new_number) 254 | game_update.update_random_number 255 | end 256 | end 257 | end 258 | 259 | describe '#maximum_guesses' do 260 | # Located inside #play_game (Public Script Method) 261 | # Query Method -> Test the return value 262 | 263 | context 'when game minimum and maximum is 1 and 10' do 264 | subject(:game_ten) { described_class.new(1, 10) } 265 | 266 | it 'returns 4' do 267 | max = game_ten.maximum_guesses 268 | expect(max).to eq(4) 269 | end 270 | end 271 | 272 | context 'when game minimum and maximum is 1 and 100' do 273 | subject(:game_hundred) { described_class.new(1, 100) } 274 | 275 | it 'returns 7' do 276 | max = game_hundred.maximum_guesses 277 | expect(max).to eq(7) 278 | end 279 | end 280 | 281 | # ASSIGNMENT #3 282 | 283 | # Write a test for the following context. 284 | context 'when game minimum and maximum is 100 and 600' do 285 | subject(:game_six_hundred) { described_class.new(100, 600) } 286 | 287 | it 'returns 9' do 288 | max = game_six_hundred.maximum_guesses 289 | expect(max).to eq(9) 290 | end 291 | end 292 | end 293 | 294 | describe '#create_binary_search' do 295 | # Located inside #play_game (Public Script Method) 296 | # Method with Outgoing Command -> Test that a message is sent 297 | 298 | subject(:game_create) { described_class.new(1, 10, number_create) } 299 | let(:number_create) { instance_double(RandomNumber) } 300 | 301 | # Since a new BinarySearch is given a RandomNumber, we can test that it 302 | # receives the correct double. 303 | it 'creates a new BinarySearch with RandomNumber double' do 304 | expect(BinarySearch).to receive(:new).with(1, 10, number_create) 305 | game_create.create_binary_search 306 | end 307 | end 308 | 309 | describe '#display_binary_search' do 310 | # Located inside #play_game (Public Script Method) 311 | 312 | # Looping Script Method -> Test the behavior of the method (for example, it 313 | # stops when certain conditions are met). 314 | 315 | # This method is a Looping Script Method, because #display_turn_order will 316 | # loop until binary_search.game_over? 317 | 318 | subject(:game_display) { described_class.new(1, 10) } 319 | let(:search_display) { instance_double(BinarySearch) } 320 | 321 | context 'when game_over? is false once' do 322 | before do 323 | allow(search_display).to receive(:game_over?).and_return(false, true) 324 | end 325 | 326 | it 'calls display_turn_order one time' do 327 | expect(game_display).to receive(:display_turn_order).with(search_display).once 328 | game_display.display_binary_search(search_display) 329 | end 330 | end 331 | 332 | context 'when game_over? is false twice' do 333 | before do 334 | allow(search_display).to receive(:game_over?).and_return(false, false, true) 335 | end 336 | 337 | it 'calls display_turn_order two times' do 338 | expect(game_display).to receive(:display_turn_order).with(search_display).twice 339 | game_display.display_binary_search(search_display) 340 | end 341 | end 342 | 343 | # ASSIGNMENT #4 344 | 345 | # Write a test for the following context. 346 | context 'when game_over? is false five times' do 347 | before do 348 | allow(search_display).to receive(:game_over?).and_return(false, false, false, false, false, true) 349 | end 350 | 351 | it 'calls display_turn_order five times' do 352 | expect(game_display).to receive(:display_turn_order).with(search_display).exactly(5).times 353 | game_display.display_binary_search(search_display) 354 | end 355 | end 356 | end 357 | 358 | # ASSIGNMENT #5 359 | 360 | # Write three tests for the following method. 361 | describe '#display_turn_order' do 362 | # This method is both a Command Method and a Script Method. It changes the 363 | # observable state by incrementing the instance variable @guess_count by one, 364 | # sends two command messages #make_guess and #update_range to the object 365 | # binary_search of class BinarySearch, and sends one command message to `self` 366 | # by calling #display_guess. 367 | 368 | # Create a new subject and an instance_double for BinarySearch. 369 | 370 | subject(:game_turn) { described_class.new(1, 10) } 371 | let(:binary_search_turn) { instance_double(BinarySearch) } 372 | 373 | before do 374 | allow(binary_search_turn).to receive(:make_guess) 375 | allow(binary_search_turn).to receive(:update_range) 376 | allow(game_turn).to receive(:display_guess) 377 | end 378 | 379 | # Command Method -> Test the change in the observable state 380 | it 'increases guess_count by one' do 381 | expect { game_turn.display_turn_order(binary_search_turn) }.to change { game_turn.instance_variable_get(:@guess_count) }.by(1) 382 | end 383 | 384 | # Method with Outgoing Command -> Test that a message is sent 385 | it 'sends make_guess to binary_search_turn' do 386 | expect(binary_search_turn).to receive(:make_guess).once 387 | game_turn.display_turn_order(binary_search_turn) 388 | end 389 | 390 | # Method with Outgoing Command -> Test that a message is sent 391 | it 'sends update_range to binary_search_turn' do 392 | expect(binary_search_turn).to receive(:update_range).once 393 | game_turn.display_turn_order(binary_search_turn) 394 | end 395 | 396 | # Using method expectations can be confusing. Stubbing the methods above 397 | # does not cause this test to pass; it only 'allows' a method to be 398 | # called, if it is called. 399 | 400 | # To test this fact, let's allow a method that is not called in 401 | # #display_turn_order. Uncomment the line at the bottom of this 402 | # paragraph, move it to the before hook, and run the tests. 403 | # All of the tests should continue to pass. Replace 'binary_search_turn' 404 | # with whatever you named your BinarySearch instance_double if necessary. 405 | # allow(binary_search_turn).to receive(:game_over?) 406 | 407 | # Now, in the lib/15a_binary_game.rb file, comment out either line, 408 | # binary_search.make_guess or binary_search.update_range. Resave the file 409 | # and rerun the tests. The test for the method that you commented out will 410 | # fail because the method is never called. Now, uncomment the line & resave 411 | # the file to have all tests passing. 412 | end 413 | 414 | describe '#introduction' do 415 | # Located inside #play_game (Public Script Method) 416 | # Only contains puts statements -> No test necessary & can be private. 417 | end 418 | 419 | describe '#display_guess' do 420 | # Located inside #display_turn_order (Looping Script Method) 421 | # Only contains puts statements -> No test necessary & can be private. 422 | end 423 | 424 | describe '#print_number' do 425 | # Only contains puts statements -> No test necessary & can be private. 426 | end 427 | end 428 | -------------------------------------------------------------------------------- /spec_answers/16a_caesar_answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../lib/16a_caesar_breaker' 4 | require_relative '../lib/16b_caesar_translator' 5 | 6 | # The file order to complete this lesson: 7 | 8 | # 1. Familiarize yourself with the four lib/16 files. 9 | # - lib/16_caesar_main 10 | # - lib/16a_caesar_breaker 11 | # - lib/16b_caesar_translator which is based on the typical Caesar Cipher 12 | # - lib/16c_database (module) 13 | # 2. Check out the completed tests in spec/16b_caesar_translator. 14 | # 3. Complete spec/16a_caesar_breaker_spec. 15 | 16 | # The CaesarBreaker class creates the 26 possible translations (using the 17 | # CaesarTranslator class) & saves them in a yaml file (using a Database module). 18 | 19 | # Let's write tests for the CaesarBreaker class & the included Database module. 20 | 21 | # In this lesson, you will be writing tests for 3 methods - 22 | # #create_decrypted_messages, #save_decrypted_messages, and #save_to_yaml. 23 | # In addition, you will learn about testing module methods and how to handle 24 | # testing methods that can raise errors. 25 | 26 | describe CaesarBreaker do 27 | # The tests for CaesarBreaker do not depend on creating different conditions. 28 | # Therefore we can use the same subject instance and CaesarTranslator double. 29 | 30 | subject(:phrase) { described_class.new('Lipps, Asvph!', translator) } 31 | let(:translator) { instance_double(CaesarTranslator) } 32 | 33 | describe '#decrypt' do 34 | # Public Script Method -> No test necessary, but all methods inside should 35 | # be tested. 36 | end 37 | 38 | # ASSIGNMENT #1 39 | 40 | # Write a test for the following method. 41 | describe '#create_decrypted_messages' do 42 | # This is a Looping Script Method. 43 | # Located inside #decrypt (Public Script Method) 44 | 45 | # Method with Outgoing Command -> Test that a message is sent 46 | it 'sends translate 26 times' do 47 | expect(translator).to receive(:translate).exactly(26).times 48 | phrase.create_decrypted_messages 49 | end 50 | end 51 | 52 | # MODULE TESTING: There are several ways to test methods inside a module. 53 | # (The following methods are located in lib/16c_database.rb) 54 | 55 | # Some prefer explicitly including it in the configuration option. 56 | # https://rspec.info/features/3-12/rspec-core/helper-methods/modules/ 57 | 58 | # Some prefer testing modules using a dummy class. 59 | # https://mixandgo.com/learn/how-to-test-ruby-modules-with-rspec 60 | 61 | # Modules can also be tested in a class that includes it, which is how the 62 | # following tests work. 63 | 64 | describe '#save_decrypted_messages' do 65 | # Located inside #decrypt (Public Script Method) 66 | 67 | # Method with Outgoing Commands -> Test that the messages are sent 68 | context 'when the directory does not exist' do 69 | # We need to stub several outgoing commands that communicate with 70 | # external systems and create the conditions for this test. In addition, 71 | # there are puts statements in #display_file_location so let's stub that 72 | # method to clean up the test output. 73 | before do 74 | allow(Dir).to receive(:exist?).and_return(false) 75 | allow(Dir).to receive(:mkdir) 76 | allow(File).to receive(:open) 77 | allow(phrase).to receive(:display_file_location) 78 | end 79 | 80 | # ASSIGNMENT #2 81 | # Write the following 3 tests: 82 | 83 | it 'sends message to check the existence of the 16_cipher directory' do 84 | expect(Dir).to receive(:exist?).with('16_cipher').exactly(1).time 85 | phrase.save_decrypted_messages 86 | end 87 | 88 | it 'sends message to create a directory' do 89 | expect(Dir).to receive(:mkdir).exactly(1).time 90 | phrase.save_decrypted_messages 91 | end 92 | 93 | it 'sends message to create a file' do 94 | expect(File).to receive(:open).exactly(1).time 95 | phrase.save_decrypted_messages 96 | end 97 | end 98 | 99 | # ASSIGNMENT #3 100 | # Write the following 3 tests and the shared before block: 101 | 102 | # Method with Outgoing Commands -> Test that the messages are sent 103 | context 'when the directory exists' do 104 | before do 105 | allow(Dir).to receive(:exist?).and_return(true) 106 | allow(File).to receive(:open) 107 | allow(phrase).to receive(:display_file_location) 108 | end 109 | 110 | it 'sends message to check the existence of the 16_cipher directory' do 111 | expect(Dir).to receive(:exist?).with('16_cipher').exactly(1).time 112 | phrase.save_decrypted_messages 113 | end 114 | 115 | it 'does not send message to create a directory' do 116 | expect(Dir).not_to receive(:mkdir) 117 | phrase.save_decrypted_messages 118 | end 119 | 120 | it 'sends message to create a file' do 121 | expect(File).to receive(:open).exactly(1).time 122 | phrase.save_decrypted_messages 123 | end 124 | end 125 | 126 | # This method has a rescue block in case an error occurs. 127 | # Let's test that this method can run without raising an error. 128 | # https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/raise-error/ 129 | 130 | context 'when file is saved successfully' do 131 | before do 132 | allow(Dir).to receive(:exist?).and_return(false) 133 | allow(Dir).to receive(:mkdir) 134 | allow(File).to receive(:open) 135 | allow(phrase).to receive(:display_file_location) 136 | end 137 | 138 | it 'does not raise an error' do 139 | expect { phrase.save_decrypted_messages }.not_to raise_error 140 | end 141 | end 142 | 143 | # Let's simulate an error occurring during #save_decrypted_messages by 144 | # allowing File.open to raise the error 'Errno::ENOENT'. This error means 145 | # that no such file or directory could be found. In addition, when an error 146 | # is rescued there are two puts to stub to clean up the test output. 147 | context 'when rescuing an error' do 148 | before do 149 | allow(Dir).to receive(:exist?).and_return(false) 150 | allow(Dir).to receive(:mkdir) 151 | allow(File).to receive(:open).and_raise(Errno::ENOENT) 152 | allow(phrase).to receive(:puts).twice 153 | end 154 | 155 | # When an error is rescued, the method will not raise an error. Therefore 156 | # you test the conditions that would occur if an error were rescued. 157 | it 'does not display file location' do 158 | expect(phrase).not_to receive(:display_file_location) 159 | phrase.save_decrypted_messages 160 | end 161 | 162 | it 'outputs two error messages' do 163 | expect(phrase).to receive(:puts).with('Error while writing to file LippsAsvph.yaml.') 164 | expect(phrase).to receive(:puts).with(Errno::ENOENT) 165 | phrase.save_decrypted_messages 166 | end 167 | end 168 | end 169 | 170 | # ASSIGNMENT #4 171 | # Write the following test: 172 | 173 | describe '#save_to_yaml' do 174 | # Method with Outgoing Command -> Test that a message is sent 175 | 176 | it 'dumps to yaml' do 177 | expect(YAML).to receive(:dump) 178 | phrase.save_to_yaml 179 | end 180 | end 181 | end 182 | --------------------------------------------------------------------------------