Creating a mock user
Once we updated our game to check for authorized player addresses, we broke most of our tests:
Failed tests:
[FAIL. Reason: Unauthorized] test_can_mark_space_with_O() (gas: 9965)
[FAIL. Reason: Unauthorized] test_can_mark_space_with_X() (gas: 9901)
[FAIL. Reason: Unauthorized] test_cannot_mark_space_with_Z() (gas: 14920)
[FAIL. Reason: Unauthorized] test_cannot_overwrite_marked_space() (gas: 9900)
[FAIL. Reason: Unauthorized] test_checks_for_antidiagonal_win() (gas: 9947)
[FAIL. Reason: Unauthorized] test_checks_for_diagonal_win() (gas: 9899)
[FAIL. Reason: Unauthorized] test_checks_for_horizontal_win() (gas: 9945)
[FAIL. Reason: Unauthorized] test_checks_for_horizontal_win_row2() (gas: 9921)
[FAIL. Reason: Unauthorized] test_checks_for_vertical_win() (gas: 9923)
[FAIL. Reason: Unauthorized] test_draw_returns_no_winner() (gas: 9900)
[FAIL. Reason: Unauthorized] test_game_in_progress_returns_no_winner() (gas: 9937)
[FAIL. Reason: Unauthorized] test_reset_board() (gas: 9965)
[FAIL. Reason: Unauthorized] test_symbols_must_alternate() (gas: 9943)
[FAIL. Reason: Unauthorized] test_tracks_current_turn() (gas: 12881)
Encountered a total of 14 failing tests, 12 tests succeeded
We could use a bunch of vm.prank
cheatcodes to stub out the address before every call to ttt.markSpace
, but that's pretty verbose. Instead, let's use another common ds-test pattern: creating a mock user.
At the top of our tests, let's create a new User
contract that stores an internal address at construction time:
contract User {
address internal _address;
constructor(address address_) {
_address = address_;
}
}
Next, let's pass in an instance of our game, and the Vm
:
contract User {
TicTacToken internal ttt;
Vm internal vm;
address internal _address;
constructor(address address_, TicTacToken _ttt, Vm _vm) {
_address = address_;
ttt = _ttt;
vm = _vm;
}
}
Let's give this contract its own markSpace
function that takes the same arguments and delegates to ttt
:
contract User {
TicTacToken internal ttt;
Vm internal vm;
address internal _address;
constructor(address address_, TicTacToken _ttt, Vm _vm) {
_address = address_;
ttt = _ttt;
vm = _vm;
}
function markSpace(uint256 space, uint256 symbol) public {
ttt.markSpace(space, symbol);
}
}
Finally, before we call ttt.markSpace
, let's use vm.prank
to set the caller address to the internal state variable we set up at construction time:
contract User {
TicTacToken internal ttt;
Vm internal vm;
address internal _address;
constructor(address address_, TicTacToken _ttt, Vm _vm) {
_address = address_;
ttt = _ttt;
vm = _vm;
}
function markSpace(uint256 space, uint256 symbol) public {
vm.prank(_address);
ttt.markSpace(space, symbol);
}
}
We've created a helper contract that can act as a mock user in our tests. Let's create variables for playerX
and playerO
at the top of our tests and create them in the setUp
method:
User internal playerX;
User internal playerO;
function setUp() public {
ttt = new TicTacToken(OWNER, PLAYER_X, PLAYER_O);
playerX = new User(PLAYER_X, ttt, vm);
playerO = new User(PLAYER_O, ttt, vm);
}
Now, rather than call vm.prank
over and over, we can use our helper contracts to simulate user calls instead:
function test_can_mark_space_with_X() public {
playerX.markSpace(0, X);
assertEq(ttt.board(0), X);
}
function test_can_mark_space_with_O() public {
playerX.markSpace(0, X);
playerO.markSpace(1, O);
assertEq(ttt.board(1), O);
}
function test_cannot_mark_space_with_Z() public {
vm.expectRevert("Invalid symbol");
playerX.markSpace(0, 3);
}
function test_cannot_overwrite_marked_space() public {
playerX.markSpace(0, X);
vm.expectRevert("Already marked");
playerO.markSpace(0, O);
}
function test_symbols_must_alternate() public {
playerX.markSpace(0, X);
vm.expectRevert("Not your turn");
playerO.markSpace(1, X);
}
function test_tracks_current_turn() public {
assertEq(ttt.currentTurn(), X);
playerX.markSpace(0, X);
assertEq(ttt.currentTurn(), O);
playerO.markSpace(1, O);
assertEq(ttt.currentTurn(), X);
}
...hopefully you get the idea: throughout our tests, we'll update ttt
to playerX
or playerO
if we need the call to originate from a specific player.
Now that our tests are fixed up, let's take on a refactor and simplify some of our validations.