├── .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 |
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 |
--------------------------------------------------------------------------------