RSpec:使用方法存根来测试交互式用户输入

时间:2018-04-09 09:33:21

标签: ruby rspec

我是Ruby和RSpec的新手,已经花了好几个小时尝试编写测试交互式tic tac toe program的第一步,但是没有找到任何有用的答案来解决我得到的错误:

> bundle exec rspec --format documentation                                
Do you want to play tic-tac-toe? (y/n)

An error occurred while loading ./spec/tic_tac_toe_spec.rb.
Failure/Error: choice = gets.chomp.downcase

Errno::ENOENT:
  No such file or directory @ rb_sysopen - --format
# ./lib/tic_tac_toe.rb:70:in `gets'
# ./lib/tic_tac_toe.rb:70:in `gets'
# ./lib/tic_tac_toe.rb:70:in `start_game'
# ./lib/tic_tac_toe.rb:144:in `<top (required)>'
# ./spec/tic_tac_toe_spec.rb:1:in `require'
# ./spec/tic_tac_toe_spec.rb:1:in `<top (required)>'
No examples found.

Finished in 0.0004 seconds (files took 0.08415 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples

当您开始游戏时,它会提示您是否要播放(是/否)。我已经尝试过使用模拟和存根的不同变体但总是会得到同样的错误。我想我错过了一些非常基本但却看不到的东西。

问题:如何充分解决(使用RSpec)游戏的第一个提示?

游戏使用独立的方法(start_game)但不是一个类来启动。我正在寻找某种方式模拟或存储一些默认输入(如yn),因为我希望测试自动化。我看到的所有示例都使用类来启动程序(而不是像这里的独立方法)。

目前在spec/tic_tac_toe_spec.rb let :output:gamechoice = gets.chomp.downcase个关键字,我在某个地方找到了一个示例,但显然这不起作用。

修改我想测试以下方法。 RSpec代码在def start_game puts "Do you want to play tic-tac-toe? (y/n)" choice = gets.chomp.downcase unless ["y","n"].include?(choice) abort("Please answer with y or n.") end case choice when "y" puts "Ok, let's start!" b = Board.new puts "Enter name of player 1 (O)" player1name = gets.chomp player1 = Player.new(player1name) puts "Now enter name of player 2 (X)" player2name = gets.chomp player2 = Player.new(player2name) play_game(player1, player2, b) when "n" puts "Your loss." end end #start_game 行上保持窒息。

import numpy as np


training_set = np.array([[0, 1, 0, 1, 0, 1],[0, 0, 0, 1, 0, 0],[0, 0, 0, 0, 1, 0],[1, 0, 1, 0, 1, 0],[0, 1, 1, 1, 0, 1],[0, 1, 0, 0, 1, 1],[1, 1, 1, 0, 0, 0],[1, 1, 1, 1, 0, 1],[0, 1, 1, 0, 1, 0],[1, 1, 0, 0, 0, 1],[1, 0, 0, 0, 1, 0]])

def p(X):
   Fx = X[:,X.shape[1]-1]
   x0= 0
   x1= 0
   for i in range(len(Fx)):
       if Fx[i-1] == 1:
           x0 = x0+1
       else:
           x1 = x1+1
   P0 = x0/len(Fx)
   P1 = x1/len(Fx)      
   return(P0,P1)    

def H(X):
    result = -p(X)[0]*np.log(p(X)[0])-p(X)[1]*np.log(p(X)[1]) #needs to be log2
    print("1 = pure, 0 = unpure 1/2 = decision can be random:  Calculating Entropy: -" + str(p(X)[0]) + "*" + str(np.log(p(X)[0])) + "-" + str(p(X)[1]) + "*" + str(np.log(p(X)[1])) )
    return result

def Q(X,i):
    Xi = X[:,i]
    result0= 0
    result1= 0
    for j in range(len(Xi)):
        if Xi[j] == 1:
            result1 = result1 + len(X[i,:])
        else: result0 = result0 + len(X[i,:])
    result1 = result1/len(X)
    result0 = result0/len(X)
    return(result0,result1) 

def X_column(X,i,v):
    list = X[np.where(X[:,i] == v)]
    return list

def IG(X,i):
    result = H(X)-Q(X,i)[0]*H(X_column(X,i,0))-Q(X,i)[1]*H(X_column(X,i,1))
    return result

#To teach decision trees on learning set S, we will used following alorithm(ID3):
#    1. There is example set S
#    2. If |{f(x) : (x, f(x)) ∈ S}| = 1= 1 create leaf with label f(x)
#    3. For i = 1,2,...,n calculate value IG(S,i)
#    4. May j be an index o fthe biggest of calculated values
#    5. Set node with label Xj
#    6. For subsets:
#        S0 = {(x, f(x)) ∈ S : xj = 0}
#        and
#        S1 = {(x, f(x)) ∈ S : xj = 1}
#        run algorithm recurrent (for S ← S0 i S ← S1) and add new nodes as a childs for a node with label j

def ID3(S, recursion = 0, label = 0, tree = np.array()):
    result = np.array()
    recursion += 1
    rows = S.shape[0]
    columns = S.shape[1]      
    if S[:,columns-1] == True:
        tree[recursion]= S[0,columns-1]
        break    
    for i in range(rows):
        result[i]= IG(S,i)
    j = result.max()
    tree[recursion]= 1 #czym jest etykieta xj
    S0 = X_column(S,i,0)
    tree[recursion+1] = ID3(S0,recursion = recursion )
    S1 = X_column(S,i,1)
    tree[recursion+2] = ID3(S1,recursion = recursion)
    return tree

def pruning():
    return tree

3 个答案:

答案 0 :(得分:2)

您收到错误是因为您将--format documentation作为参数传递给RSpec。

gets读取已通过的ARGV。这还包括RSpec参数。您应该使用STDIN.gets,这样您才能阅读标准输入而不是参数。

您可以在此问题中详细了解: https://stackoverflow.com/a/19799223/6156030

答案 1 :(得分:1)

一个简单的方法可能是:

it 'works' do
  allow($stdin).to receive(:gets).and_return('y')

  expect { start_game } # (Or however you run it!)
    .to output("Ok, let's start!")
    .to_stdout
end

您还可以为gets指定多个返回值,这些值将在测试运行时按顺序使用。

你似乎已经开始(但没有完全实现)的另一种方法是在游戏中注入一个明确的output(可能是一天input)对象。

从纯粹主义者的角度来看,这确实是一种更清晰的方法 - 即不会像$stdin那样存在全局对象 - 但对于您的初始版本的代码来说可能有点过分。除非你打算做一些像运行并行规范这样的事情,否则我不会为此担心。

编辑:在更详细地查看您的实际代码之后,我看到了问题。您正在定义全局方法,这些方法正在执行多项操作且紧密耦合。这使得编写测试更加困难!

这里我添加了一个工作测试示例:

https://github.com/tom-lord/tic_tac_toe_rspec/commit/840df0b7f1380296db97feff0cd3ca995c5c6ee3

然而,为了简化这一点,我的建议是在适当的类中定义所有每个方法,并使代码更少程序化。 (即,只是让一个方法的结束以长序列调用下一个方法!)这种重构可能超出了StackOverflow答案的范围。

答案 2 :(得分:0)

您需要在规范中使用stub and mock gets方法:

yes_gets = double("yes_gets")
allow($stdin).to receive(:gets).and_return(yes_gets)

然后您可以回复#chomp

expect(yes_gets).to receive(:chomp).and_return('Y')

您可以通过返回此双重对象来覆盖downcase的类似方法调用。

您也可以为'N'情况做模拟对象的类似工作,当玩家输入N(否)时,您希望游戏退出:

no_gets = double("no_gets")
allow($stdin).to receive(:gets).and_return(no_gets)
expect(no_gets).to receive(:chomp).and_return('N')