Building Tic-Tac-Toe in Rust - Part 2 - Testing the Game
Building Tic-Tac-Toe in Rust: Part 2 - Testing the Game
In the first part of this series, we built a text-based version of Tic-Tac-Toe in Rust. Now, it’s time to take our game to the next level by adding unit tests. Testing ensures that our game works as expected and remains reliable as we add more features.
In this post, we’ll:
- Write unit tests for the core game logic.
- Refactor the code for better testability.
- Identify and fix potential edge cases.
Step 1: Set Up Testing
Rust’s built-in test framework makes it easy to write and run tests. To get started:
- Add a
mod tests
block to yoursrc/main.rs
file. - Annotate each test function with
#[test]
.
Example:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn example_test() {
assert_eq!(2 + 2, 4);
}
}
Run your tests with:
cargo test
Step 2: Write Tests for Game Logic
Let’s test the most critical parts of our Tic-Tac-Toe game.
1. Testing check_winner
We’ll verify that the check_winner
function correctly identifies winning combinations.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_winner_row() {
let mut game = GameState::new();
game.board = vec![
Some('X'), Some('X'), Some('X'),
None, None, None,
None, None, None,
];
assert_eq!(game.check_winner(), Some('X'));
}
#[test]
fn test_check_winner_diagonal() {
let mut game = GameState::new();
game.board = vec![
Some('O'), None, None,
None, Some('O'), None,
None, None, Some('O'),
];
assert_eq!(game.check_winner(), Some('O'));
}
#[test]
fn test_no_winner() {
let mut game = GameState::new();
game.board = vec![
Some('X'), Some('O'), Some('X'),
None, Some('X'), None,
None, None, Some('O'),
];
assert_eq!(game.check_winner(), None);
}
}
2. Testing Input Validation
We’ll ensure invalid inputs are handled gracefully by GameState
parsing.
#[test]
fn test_invalid_input() {
let input = "10";
assert!(matches!(GameState::from_str(input), Err(_)));
}
Step 3: Implement FromStr
for GameState
To simplify parsing and improve modularity, we’ll implement the FromStr
trait for GameState
. This refactor will make testing and initialization cleaner.
Updated FromStr
implementation:
use std::str::FromStr;
impl FromStr for GameState {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut board = vec![None; 9];
let mut current_player = 'X';
let trimmed_input = input.trim();
if !trimmed_input.is_empty() {
for (i, ch) in trimmed_input.chars().enumerate() {
if i >= 9 {
return Err("Input too long".to_string());
}
match ch {
'X' | 'O' => board[i] = Some(ch),
'_' => (), // Empty square
_ => return Err("Invalid character in input".to_string()),
}
}
// Infer current player based on counts
let x_count = board.iter().filter(|&&sq| sq == Some('X')).count();
let o_count = board.iter().filter(|&&sq| sq == Some('O')).count();
current_player = if x_count > o_count { 'O' } else { 'X' };
}
Ok(GameState { board, current_player })
}
}
Test the updated logic:
#[test]
fn test_from_str_valid() {
let input = "XOX____OX";
let game = GameState::from_str(input).unwrap();
assert_eq!(game.board[0], Some('X'));
assert_eq!(game.board[8], Some('X'));
assert_eq!(game.current_player, 'O');
}
#[test]
fn test_from_str_invalid_character() {
let input = "XOQ____OX";
assert!(GameState::from_str(input).is_err());
}
#[test]
fn test_from_str_empty() {
let input = "_________";
let game = GameState::from_str(input).unwrap();
assert!(game.board.iter().all(|&sq| sq.is_none()));
assert_eq!(game.current_player, 'X');
}
Step 4: Run the Tests
Run your tests with:
cargo test
You should see output like:
test tests::test_check_winner_row ... ok
test tests::test_check_winner_diagonal ... ok
test tests::test_no_winner ... ok
test tests::test_from_str_valid ... ok
test tests::test_from_str_invalid_character ... ok
test tests::test_from_str_empty ... ok
Wrapping Up
With the FromStr
implementation for GameState
, our Tic-Tac-Toe game is now easier to initialize and test. We’ve:
- Implemented
FromStr
to handle board parsing and initialization. - Tested critical game logic like checking winners and parsing inputs.
- Ensured our game gracefully handles invalid scenarios.
In the next post, we’ll explore adding features like saving and loading game states. Until then, try adding more tests or experimenting with additional features!
As always, you can find the code for this post on GitHub: Tic-Tac-Toe Repository.