We've got two different mock objects being created here, one for our user interface, and another for our grid. You can see that we set the interface of the game object using mock("UI"), which is a blank mock object created by RSpec. The following method fills in our expectations:
def mock_player_selection @game.interface.should_receive(:get_players).and_return(["Gregory","Joe"]) end
Here we're basically creating a simple stubbed out object, where we expect that
some_interface.get_players will be called, and return an array with some names in it. We're using this to test how our game will behave upon startup, and also to test some simple logic:
it "should have an array of players" do @game.players.should == ["Gregory","Joe"] end it "should set the first player entered via the UI to the current_player" do @game.current_player.should == "Gregory" end
Our mock objects will self-verify, and they'll also help us by responding things we're expecting so that we can form examples around them. It's worth keeping in mind that a mock isn't going to automatically return meaningful data--you need to take care that what you're asking for is really what you expect from your dependent classes.
Let's take a quick look at the implementation of Game so far:
module Dots class Game attr_accessor :interface attr_reader :players, :grid def start @players = interface.get_players @turn = 0 rows,cols = interface.get_grid_size @grid = Dots::Grid.new(rows,cols) end def current_player @players[@turn] end end end
You can see where
interface.get_players is being called, and also where our grid interaction happens. If you look over the examples, it should be pretty clear what's going on here. There is one small trick we do in our examples that's worth revisiting, though.
Adding mock calls to existing classes
With our interface code, it's very clear what's happening so long as you're familiar with duck typing. We're passing in an object that acts like something else, and Ruby is happy to work with it. However, if you look at Game#start, you can see that our Grid objects are explicitly instantiated.
We'd like to avoid testing these objects directly, for a number of reasons. Perhaps the most important is that we should be able to focus on just the interactions with that object, and not its implementation details. Rather than using a technique such as dependency injection or some other design workaround to get at this code and insert our mock objects, RSpec offers us a shortcut:
def mock_prompt_for_grid_size grid = mock("grid") @game.interface.should_receive(:get_grid_size).and_return([8,10]) Dots::Grid.should_receive(:new).with(8,10).and_return(grid) end
By stubbing out the constructor call, we can have it return a mock object, which we can then define our expectations on. This largely decouples us from the class without any additional work, and with some extra boilerplate, could actually completely separate us from its implementation.
As we evolve the game class, the interactions get more complex. To cope with this, we encapsulate our helper methods in a module, which can then be included in our example sets as needed.
The final set of mock helpers for the Game specs look like this:
module GameStateMocks def mock_player_selection @game.interface.should_receive(:get_players).and_return(["Gregory","Joe"]) end def mock_prompt_for_grid_size grid = mock("grid") @game.interface.should_receive(:get_grid_size).and_return([8,10]) Dots::Grid.should_receive(:new).with(8,10).and_return(grid) end def mock_initial_game_state @game.interface = mock("UI") mock_player_selection mock_prompt_for_grid_size @game.interface.should_receive(:update_display).with(an_instance_of(Dots::Game)) end def stub_move(edge,boxes_completed=0) @game.interface = mock("UI") box_set = mock("box set") if boxes_completed.zero? box_set.should_receive(:empty?).and_return(true) else box_set.should_receive(:empty?).and_return(false) box_set.should_receive(:size).and_return(boxes_completed) end @game.interface.should_receive(:get_move).and_return(edge) @game.interface.should_receive(:update_display).with(an_instance_of(Dots::Game)) @game.grid.should_receive(:connect).with(*edge).and_return(box_set) @game.move end end
As you can see here, we've defined a whole bunch of behaviors, some for objects that don't exist yet, others for objects that we only want to test our interactions with. The key here is to mock out all the interactions you need to run your specs standalone if needed, but no more. If your code doesn't absolutely depend on a certain behavior, there is no need to mock it out.
Although it might be tricky to get your mocks right, they make your specs and implementation code very simple and expressive. Take the following specs, for example:
describe "A Game in progress with two players" do include GameStateMocks before :each do @game = Dots::Game.new mock_initial_game_state @game.start end it "should alternate players when a box is not completed" do @game.current_player.should == "Gregory" stub_move([[0,0],[0,1]],0) @game.current_player.should == "Joe" stub_move([[0,1],[1,1]],0) @game.current_player.should == "Gregory" end it "should not change players if 1 box is completed" do @game.current_player.should == "Gregory" stub_move([[0,0],[0,1]],1) @game.current_player.should == "Gregory" end it "should not change players if 2 boxes are completed" do @game.current_player.should == "Gregory" stub_move([[0,0],[0,1]],2) @game.current_player.should == "Gregory" end end
You can see that we're stubbing out moves on the grid, clearly expressing how turn order should progress. Though the actual interactions here are simple, imagine how handy a mock could be when dealing with real-world resources you can't meaningfully test live against, such as web services. As long as you can account for the interactions you expect (return values, error conditions, etc.), you can create mock objects to mimic the behaviors and fully test your code without direct ties to those resources.
Until Next Time…
I've kept my focus mostly on exposing the interesting test bits, but what you'll find in this package is a fully functional game, albiet a little simple and fragile. Because it's broken up by iteration, you can hopefully get a good feel for the workflow. I've even implemented a simple text-based UI using HighLine for the game, which I've not covered here. Interestingly enough, this didn't require me to make many modifications at all to the rest of the code, even though I built it without a UI hooked up.
The end result looked something like this:
0 1 2 3 4 5 + - + - + - + - + - + 5 - - - - - - + - + - + - + - + - + 4 - - - - - - + - + - + - + - + - + 3 - - - - - - + - + - + - + - + - + 2 - - - - - - +===+ - + - + - + - + 1 | G | - - - - +===+===+ - + - + - + 0 Current Player: Greg Scores: ( Greg: 1 Joe: 0) Draw from (x,y):
You can find this in iteration 16, runnable via
play_dots.rb. See the underlying code for more details.
We'll be back soon to wrap up this exploration of BDD by looking at some of the advanced tools and techniques that experienced users leverage to improve their process. For now, hopefully you've gained a decent understanding of how things work in practice, and will have fun playing Dots and Boxes.
Return to Ruby.
Showing messages 1 through 1 of 1.
Iteration 16 is missing interface_spec
2007-10-21 15:44:06 gfleshman [View]