how to write rspec tests for one method - ruby

I have this method as part of a larger class. I'm trying to write a test for it but I'm new to rspec and I'm kinda stumped...I can test 'drawgrid' if I comment out everything in the 9.times loop. but if I uncomment that code the current test fails. I need to test that the play method...runs the game. that it puts 'drawgrid'...runs the game sequence 9 times putting the 'drawgrid' after each turn. But I'm not sure how to do this. Any pointers are greatly appreciated.
Below is the play method and it's current spec
def play
#draw the board
puts drawgrid
#make a move
turn = 0
9.times do
if turn.even?
#player = #player_h.move_human("X", #board)
#move = #player.to_sym
#marker = #player_h.boardpiece
does_move_exist(#move,#marker)
is_a_human_win(#board)
else
#player = #player_c.move_computer("O", #board)
#move = #player
#marker = #player_c.boardpiece
does_move_exist(#move,#marker)
is_a_computer_win(#board)
end
puts drawgrid
turn += 1
end # 9.times ends
end
current spec....
describe 'play method' do
it 'draws the game grid' do
#player_human = Player.new('X')
#player_computer = Player.new('O')
#board = Board.new
#game = Game.new(#player_human, #player_computer, #board)
#game.should_receive(:puts).with("\na | | \n----------\nb | | \n----------\nc | | \n----------\n 1 2 3\n")
#game.play
end
end
describe '9.times' do
it 'runs game sequence 9 times...once per board spot' do
#player_human2 = Player.new('X')
#player_computer2 = Player.new('O')
#board2 = Board.new
#game2 = Game.new(#player_human2, #player_computer2, #board2)
turn = 0
9.times do
if turn.even?
#player_human2.should_receive(:puts).with("human move...")
#player_human2.stub(:gets).and_return("b2")
else
#player_human2.should_receive(:puts).with("computer move...")
#player_human2.stub(:gets).and_return("a1")
end
turn += 1
end
#game2.play
end
end

In general, I feel like both your code and your test are trying to do too much in one method. The interesting bit about your play method isn't so much the 9 times as what happens inside of that loop. My first suggestion for refactoring that is to turn what's inside that loop into a method called "take_turn" or something similar.
Then you could write specs for what happens for a single turn. And, your spec for the play method would test that the take_turn method is called 9 times.
That's not to say that you couldn't keep your code the way that it is and write an effective test for it... you just can't be super surgical about what you're testing.
Hope that helps.

I second what Dave says. Try to simplify. Basically, if you can simplify your play method, it will simplify your test. Right now play is concerned with the implementation details of each turn. Push those details down, and your test can become easier to write and more granular. I'm not the best, and I'm not super happy with this yet, but hopefully the code below pushes you in the right direction:
#play.rb
class Board
end
class Player
def initialize(symbol)
#symbol = symbol
end
def take_turn
end
end
class Game
def initialize(player1, player2, board)
#player1, #player2, #board = player1, player2, board
end
def play
drawgrid
(0...9).each do |turn|
turn.even? ? #player1.take_turn : #player2.take_turn
drawgrid
end
end
def drawgrid
end
end
And the test file:
#play_spec.rb
require './play.rb'
describe '#play' do
before do
#player1 = Player.new('X')
#player2 = Player.new('O')
#game = Game.new(#player1, #player2, Board.new)
end
it 'draws the game grid' do
#game.should_receive(:drawgrid).at_least(:once)
#game.play
end
it 'runs game sequence 9 times...once per board spot' do
#player1.stub(take_turn: true)
#player2.stub(take_turn: true)
#player1.should_receive(:take_turn).exactly(5).times
#player2.should_receive(:take_turn).exactly(4).times
#game.play
end
end

Related

Hardcoding my value in rspec is not working

My following class is like so:
attr_reader :player, :player_choice, :cpu_choice, :choices, :game, :result
def initialize(player)
#player = player
#player_choice = ""
#cpu_choice = ""
#choices = Choices.new
#result = ""
end
def get_result
#result
end
def show_cpu_choice
#cpu_choice
end
def set_player_choice(choice)
#player_choice = choice
set_cpu_choice
decide_winner
end
def set_cpu_choice
#cpu_choice = #choices.get_choices.sample
end
I have omitted any irrelevant methods but basically I want to hardcode my #cpu_choice to "Scissors" for example so this following test can work,since my cpu choice is randomly generated but it is not working whichever method i am trying in rspec.
My set_cpu_choice randomonises from an array from an instance variable in my Choices class btw.
let(:game) {Game.new("Johnny")}
describe 'Player wins' do
it 'Player selects Rock and CPU has picked Scissors' do
game.set_player_choice("Rock")
allow(game).to receive(:show_cpu_choice).and_return("Scissors")
expect(game.get_result).to eq("Johnny")
end
end
I have tried the above in my rspec and I have also tried to do it using instance_variable_set but my test still keeps randomising the cpu choice.
I have also looked at something called srand but that looks too complicated for me since I dont understand any of it.
So I'll assume that decide_winner calls show_cpu_choice and sets #result. The problem is that when set_player_choice is called game hasn't yet been set up to return "Scissors". The allow should be moved before the call to set_player_choice

Text-Based game in Ruby - Developing a battle system

I am currently trying to implement a battle system within a text-based game that I am writing. The player will go from room to room, and sometimes face multiple opponents.
I would like to:
have the player start off with a max number of hit points, and have that decline as the game progresses
pre-determine the strength (max hit points) of each opponent
have the player face many opponents at a time
This is what I have so far, but I am having a lot of difficulty conceptualizing the interaction between the player and the opponents. Also, how would I have the player face multiple opponents in succession?
Any tips would help quite a lot!
Thanks!
GF
Code:
class Player
attr_accessor :hit_points, :attack_power
def initialize
#hit_points = MAX_HIT_POINTS
#attack_power = rand(2 .. 15)
end
def alive?
#hit_points > 0
end
def hurt
#hit_points -= amount
end
def print_status
puts "*" * 80
puts "HP: #{#hit_points}/#{MAX_HIT_POINTS}"
puts "*" * 80
end
end
class Opponent
attr_accessor :hit_points, :attack_power
def initialize
#hit_points = MAX_HIT_POINTS
#attack_power = rand(1 .. 10)
end
def alive?
#hit_points > 0
end
def hurt
#hit_points -= amount
end
def interact(player)
player_damage_done = 0
player_damage_taken = 0
while player.alive?
hurt(player.attack_power)
player_damage_done += player.attack_power
break unless alive?
player.hurt(#attack_power)
player_damage_taken += #attack_power
end
if player.alive?
print "You took #{player_damage_taken} damage and dealt # {player_damage_done} damage, killing your opponent."
print "\n"
player.addPoints(player_damage_taken + player_damage_done)
else
print "Your opponent was too powerful and you died."
death
end
end
end
You should probably have some kind of environment class to keep track of all your characters. This could be expanded upon to allow movement, etc. Something super simple could look like this:
class Environment
def initialize(player, baddies)
#player = player
#baddies = baddies
end
def play_game
#baddies.each do |baddie|
baddie.interact(#player)
end
end
end
baddies = 3.times.map do
Opponent.new
end
Environment.new(Player.new, baddies).play_game
Also, your code as presented won't work. Your hurt methods are acting like they accept an amount parameter, but you don't ever declare that; you call player.add_points, but don't define that method.. Let me know if you have any other specific questions

Trouble getting tests to run properly

I've been trying to test a program that simulates an elevator for two days now with little success. Here's my elevator class, the program is still a work in progress and I've also commented out some methods that might not be essential to the test I'm having trouble with. I'll gladly show more code if you think it's needed
class Elevator
attr_accessor :current_floor
GROUND = 0
TOP = 15
def initialize
#floors = [] # list of floors to travel to
#pending = [] # store floors not in direction of travel
#current_floor = GROUND
#going_up = true # cannot travel downward from ground floor
#going_down = false
end
def get_input
gets.chomp
end
def run
enter_floors
sort_floors
move_to_floor
end
def enter_floors
# prompts the user for input and calls check_floor_numbers
end
def check_floor_numbers floors
# calls validate_floors to ensure user entered '2' instead of 'two'
# if proper floor numbers have been entered this method adds the number
# to #floors array other wise it calls floor_error_message
end
def floor_error_message
puts "Please enter numbers only."
enter_floors
end
def sort_floors
# if we are on the ground floor this method sorts #floors in ascending order
# if we are on the top floor it sorts #floors in descending order
# else it calls check_direction_of_travel
end
def move_to_floor
floor = #floors[0]
if #current_floor == floor
puts "You are already on floor #{floor}"
else
print_direction
(#current_floor..floor).each { |floor| puts "...#{floor}" }
#current_floor = floor # update current_floor
#floors.delete_at(0) # remove floor from list
end
check_for_more_passengers
end
def check_for_more_passengers
puts "Are there any more passengers? (Y/N)"
answer = (get_input).upcase
answer == 'Y' ? run : check_next_move
end
def check_next_move
if #floors.empty? && #pending.empty?
end_ride
else
move_to_floor
end
end
def check_direction_of_travel
# not implemented - add floor to appropriate array depending on dir
# of travel
end
def end_ride
puts "\n\nEND."
end
def print_direction
msg = " "
#going_up ? msg = "Going Up!" : msg = "Going Down!"
puts msg
end
end
I'm trying to test that the elevator can move to a specific floor. At first I was having trouble testing input from the console without running the program itself. I asked a question about this and was referred to this answer in a different question. The answer in question extract gets.chomp to a separate method then overrides the method in the tests. I ended up with something like this:
describe "it can move to a floor" do
before do
##moves = ["2", "N"]
def get_input; ##moves.next end
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
Problem: get_input was not properly overidden and running the test suit prompted the user for input so it was suggested that I open the Elevator class in the test itself to ensure that the method was properly overridden. Attempting to do so eventually led to a test like this:
describe "it can move to a floor" do
before do
class Elevator
attr_accessor :current_floor
##moves = ["2", "N"]
def get_input; ##moves.next end
def run; end
end
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
I had to override run and add an attr_accessor for current_floor because I was getting method missing errors.
Problem: This test gives the following error:
1) Failure: it can move to a floor#test_0001_should move to floor 2
[elevator_test.rb:24]: Expected: nil Actual: 2
I've tried to tidy up the Elevator class as much as possible and keep the methods as simple as I could given the parameters of the program.
Can anyone point me in the right direction towards getting this solved, with maybe pseudocode examples (if possible) to demonstrate how I should approach this problem if the answer is to refactor.
Please bear in mind that I'd also like to implement other tests like checking that the elevator class can maintain a list of floors, or that it can change direction, in the future when you answer.
Your test class ElevatorTest is redefining the Elevator to override method get_input, but it is not opening the class Elevator defined in elevator.rb, but instead it is sort of creating a new class Elevator which happens to be defined inside the class ElevatorTest. Remember every class is also a module, so now you have a new class ElevatorTest::Elevator.
To fix this issue, I have made some changes to elevator_test.rb which is shown below.
gem 'minitest', '>= 5.0.0'
require 'minitest/spec'
require 'minitest/autorun'
require_relative 'elevator'
class Elevator
##moves = ["2", "N"].each
def get_input; ##moves.next end
end
class ElevatorTest < MiniTest::Test
def test_working
assert_equal(1, 1)
end
describe "it can move to a floor" do
before do
end
it "should move to floor 2" do
e = Elevator.new
e.run
assert_equal(e.current_floor, 2)
end
end
end
Also, please remember to use .each while defining ##moves - it returns an enumerator. We can call .next only on an enumerator

Making a Yhatzee game, array won't show up on screen

Ok so I just started learning ruby and I'm making a Yhatzee game, now this is where I'm currently at:
class Yhatzee
def dices
#dices.to_a= [
dice1=rand(1..6),
dice2=rand(1..6),
dice3=rand(1..6),
dice4=rand(1..6),
dice5=rand(1..6)
]
end
def roll_dice
#dices.to_a.each do |dice|
puts dice
end
end
end
x = Yhatzee.new
puts x.roll_dice
Now the reason i typed .to_a after the array is i kept getting a "uninitialized variable #dices" error, and that seemed to fix it, i have no idea why.
anyways on to my question, i currently don't get any errors but my program still won't print anything to the screen. I expected it to print out the value of each dice in the array... any idea what I'm doing wrong? It seems to work when i do it in a procedural style without using classes or methods so i assumed it might work if i made the 'dices' method public. But no luck.
There are a few issues here. Firstly #dices is nil because it is not set anywhere. Thus when you call #dices.to_a you will get []. Also the dices method will not work either because nil does not have a to_a= method and the local variables you are assigning in the array will be ignored.
It seems a little reading is in order but I would do something like the following: (Not the whole game just refactor of your code)
class Yhatzee
def dice
#dice = Array.new(5){rand(1..6)}
end
def roll_dice
puts dice
end
end
x = Yhatzee.new
puts x.roll_dice
There are alot of additional considerations that need to be made here but this should at least get you started. Small Example of how I would recommend expanding your logic: (I did not handle many scenarios here so don't copy paste. Just wanted to give you a more in depth look)
require 'forwardable'
module Yahtzee
module Display
def show_with_index(arr)
print arr.each_index.to_a
print "\n"
print arr
end
end
class Roll
include Display
extend Forwardable
def_delegator :#dice, :values_at
attr_reader :dice
def initialize(dice=5)
#dice = Array.new(dice){rand(1..6)}
end
def show
show_with_index(#dice)
end
end
class Turn
class << self
def start
t = Turn.new
t.show
t
end
end
attr_reader :rolls
include Display
def initialize
#roll = Roll.new
#rolls = 1
#kept = []
end
def show
#roll.show
end
def roll_again
if available_rolls_and_dice
#rolls += 1
#roll = Roll.new(5-#kept.count)
puts "Hand => #{#kept.inspect}"
show
else
puts "No Rolls left" if #rolls == 3
puts "Remove a Die to keep rolling" if #kept.count == 5
show_hand
end
end
def keep(*indices)
#kept += #roll.values_at(*indices)
end
def show_hand
show_with_index(#kept)
end
def remove(*indices)
indices.each do |idx|
#kept.delete_at(idx)
end
show_hand
end
private
def available_rolls_and_dice
#rolls < 3 && #kept.count < 5
end
end
end
The main problem with this code is that you are trying to use the #dices instance variable inside of the roll_dice method, however you are not defining the instance variable anywhere (anywhere that is being used). You have created the dices method but you are not actually instantiating it anywhere. I have outlined a fix below:
class Yhatzee
def initialize
create_dices
end
def roll_dice
#dices.each do |dice|
puts dice
end
end
private
def create_dices
#dices = Array.new(5){rand(1..6)}
end
end
x = Yhatzee.new
x.roll_dice
I have done some simple refactoring:
Created an initialize method, which creates the #dice instance variable on the class initialization.
Made the 'dices' method more descriptive and changed the method visibility to private so only the class itself is able to create the #dice.
Cleaned up the creation of the dices inside of the #dice instance variable
I have omitted the .to_a from the roll_dice method, now that we create the variable from within the class and we know that it is an array and it will be unless we explicitly redefine it.
UPDATE
Although I cleaned up the implementation of the class, it was kindly pointed out by #engineersmnky that I oversaw that the roll would return the same results each time I called the roll_dice function, I have therefore written two functions which will achieve this, one that defines an instance variable for later use and one that literally just returns the results.
class Yhatzee
def roll_dice
#dice = Array.new(5){rand(1..6)} # You will have access to this in other methods defined on the class
#dice.each {|dice| puts dice }
end
def roll_dice_two
Array.new(5){rand(1..6)}.each {|dice| puts dice } # This will return the results but will not be stored for later use
end
end
x = Yhatzee.new
x.roll_dice
x.roll_dice # Will now return a new result

Ruby Beginner - Accessing Another Class Object

I'm trying to create a simple turn-based battle in Ruby, but I keep getting stuck when it comes to classes. I tried to do this by starting with basic code and building it up around that. I was able to make it work simply enough by using regular variables and basic attack code:
player = "goodguy"
player_health = 15
player_damage = 5
enemy = "badguy"
enemy_health = 15
enemy_damage = 5
puts "#{player} attacks #{enemy} and does #{player_damage} damage."
enemy_health -= player_damage
puts "#{enemy} has #{enemy_health} remaining."
Then, I turned the attack into a function (I had to make the variables global so the function could see them):
$player = "goodguy"
$player_health = 15
$player_damage = 5
$enemy = "badguy"
$enemy_health = 15
$enemy_damage = 5
def player_attack
puts "#{$player} attacks #{$enemy} and does #{$player_damage} damage."
$enemy_health -= $player_damage
puts "#{$enemy} has #{$enemy_health} health remaining."
if $enemy_health <= 0
puts "#{$enemy} died!"
end
end
player_attack()
Next, I turned Player into a class:
class Player
attr_accessor :name; :hp; :damage
def initialize(name, hp, damage)
#name = name
#hp = hp
#damage = damage
end
def attack
puts "#{self.name} attacks #{$enemy}!"
$enemy_health -= #damage
puts $enemy_health
end
end
$enemy = "badguy"
$enemy_health = 15
$enemy_damage = 5
me = Player.new("goodguy", 15, 5)
me.attack
This is where I get stuck. When I turn Enemy into a class (basically modeled exactly after the Player class), I can't figure out how to make the two objects interact with each other. This code doesn't work, but here's the last of what I tried. The #{} variables are more to show what I'm trying to make happen than anything else:
class Player
attr_accessor :name; :hp; :damage
def initialize(name, hp, damage)
#name = name
#hp = hp
#damage = damage
end
def attack
puts "#{self.name} attacks #{badguy.name}!"
badguy.hp -= #damage
puts badguy.hp
end
end
class Enemy
attr_accessor :name; :hp; :damage
def initialize(name, hp, damage)
#name = name
#hp = hp
#damage = damage
end
def attack
puts "#{self.name} attacks #{goodguy.name}!"
player.hp -= #damage
puts player.hp
end
end
goodguy = Player.new("Nicehero", 15, 5)
badguy = Enemy.new("Eviljerk", 15, 5)
me.attack
Basically, what I want to do is make it so that the Player object can interact with the Enemy object. I can't seem to get this working when I try to make 2 classes interact with each other; Also, the #{variable.name} isn't the only thing I've tried for getting the functions to report those values, but I can't seem to find how to actually reference that object.
Obviously there's something I'm missing about how objects interact or what my code is doing vs what I think it should be doing. I would appreciate any suggestions on getting these two classes to interact or how this should be rewritten so that it functions as intended.
As alluded to by #JacobM, the problem you are encountering has to do with the inability of your classes to know about other instances of each other without you explicitly passing them as an argument. Although your initial workaround of using global variables to hold references to the enemy and the player will work, this practice is strongly discouraged because it "leaks" the logic of your program throughout the entire body of your game, which is generally undesirable (see Global Variables are Bad for a detailed explanation of why to avoid them).
By removing the $ from your code, player becomes a local variable when defined in the attack method:
def attack
puts "#{self.name} attacks #{goodguy.name}!"
player.hp -= #damage
puts player.hp
end
In this construction, the player variable that you want to reference as an instance of the Player class is actually an undefined local variable that you have declared within the method body. Because the code of your Player and Enemy classes is the same, I would recommend that you create a superclass to hold this logic:
class Piece
attr_accessor :name, :hp, :damage
def initialize(name, hp, damage)
#name = name
#hp = hp
#damage = damage
end
def attack(opponent)
opponent.hp -= #damage
puts "#{#name} attacks #{opponent.name}!"
puts "#{opponent.name}'s HP: #{opponent.hp}"
end
end
and then create subclasses for the Player and the Enemy:
class Player < Piece
end
class Enemy < Piece
end
With this construction, you can create any number of enemies and pieces and have them all interact with each other separately:
> hero = Player.new("Zeus", 1000, 100)
=> #<Player:0x007fbd33958498 #name="Zeus", #hp=1000, #damage=100>
> goul = Enemy.new("Pariah", 400, 50)
=> #<Enemy:0x007fbd33949b78 #name="Pariah", #hp=400, #damage=50>
> ghost = Enemy.new("Bane", 600, 75)
=> #<Enemy:0x007fbd33937680 #name="Bane", #hp=600, #damage=75>
> hero.attack(goul)
Zeus attacks Pariah!
Pariah's HP: 300
=> nil
As all of the code of Player and Enemy is same, I can model them in a parent class (giving it a dumb name Man, you can give it some fancy name :D) removing all the code duplicity, and than inheriting from the common class.
There can be various ways to interact between two objects. I have taken the simplest by passing the other object in attack function and start interacting with it.
I will change this code in following way:
class Man
attr_accessor :name, :hp, :damage
def initialize(name, hp, damage)
#name = name
#hp = hp
#damage = damage
end
def attack opposite_team_man
puts "#{self.name} attacks #{opposite_team_man.name}!"
opposite_team_man.hp -= #damage
puts opposite_team_man.hp
end
end
class Player < Man
end
class Enemy < Man
end
goodguy = Player.new("Nicehero", 15, 5)
badguy = Enemy.new("Eviljerk", 15, 5)
goodguy.attack badguy

Resources