Unit Testing Sudoku - should the tests try to be independent of the data structure for the Sudoku board?

Viewed 20

I'm learning unit testing and TDD in python and have got the basics mastered with pytest.

I've started a learning project of writing a sudoku solver and one thing I'm realising is that at the start I'm not currently sure how I want to store the data for the sudoku board. Many talks warn about testing behaviour over implementation. Would the board data structure count as implementation?

It's very hard to write a test that is independent of the board data structure.

For example you could store the values of each cell as an integer (eg 1) or a string (eg '1') and the board as a whole can be a dict (using cell label like 'A1' or 'C5' etc as keys), list (flat or 2d) or a 81 character string.

So I might start with

hardboard = [
    [0, 0, 0, 0, 0, 0, 4, 0, 0],
    [0, 0, 1, 0, 7, 0, 0, 9, 0],
    [5, 0, 0, 0, 3, 0, 0, 0, 6],
    [0, 8, 0, 2, 0, 0, 0, 0, 0],
    [7, 0, 0, 0, 0, 0, 9, 2, 0],
    [1, 2, 0, 6, 0, 5, 0, 0, 0],
    [0, 5, 0, 0, 0, 0, 0, 4, 0],
    [0, 7, 3, 9, 0, 8, 0, 0, 0],
    [6, 0, 0, 4, 5, 0, 2, 0, 0]
]

but then decide to use a dict

hardboard = {'A1': 0, 'A2': 0, 'A3': 0, ... 'I9': 0} 

So how should I start? The only solution I can think of is to commit to one format, at least in terms how I represent the board in tests, even if the actual code uses a different data structure later and has to translate. But in my TDD noobiness I'm unsure if this is a good idea?

1 Answers

I'm not a python developer, but still: Don't think about the board in terms of implementation details (like, this is a dictionary, array, slice, whatever).

Instead imagine that the board is an abstraction and it has some behaviors - which are in plain words - things you can do with a board:

  • place a digit in an index(i,j) that should be in 1 to 9 inclusive boundaries
  • remove a digit by index (i,j)

So its like you have a "contract" - you create a board and can execute some operations on it. Of course it has some internal implementation, but its hidden from everyone who uses it. Its your responsibility to provide a working implementation but this is what you want to test: The implementation is considered working if it "reacts" on all the operations correctly (behavior = operation = function at the level of actual programming implementation)

In this case your test (schematically might look like this):

Board Tests:
   Test 1 : Check that its impossible to put a negative i index:
     // setup
     board = ...
     // when:
     board.put(-1, 3, 8) // i-index is wrong 
     // then:
     expect exception to be thrown

   Test 2 : Check that its impossible to put a negative j index:
     // setup
     board = ...
     // when:
     board.put(0, -1, 5) // j-index is wrong 
     // then:
     expect exception to be thrown

   Test 3 : Check that its impossible to put a negative number into cell:
     // setup
     board = ...
     // when:
     board.put(0, 0, -7)
     // then:
     expect exception to be thrown

   Test 4 : Check that its impossible to put number that alreay contains another number:
     // setup
     board = ...
     // when:
     board.put(0, 0, 6)
     board.put(0, 0, 5) 
     // then:
     expect exception to be thrown

... and so on and so forth (also cover with tests for getting a cell, and any other additional behavior you create for the board).

Note, that all these tests indeed check the behavior of your abstraction rather than internal implementation.

At the level of implementation you can create a class for the board, internal data fields will contain the state, whereas the behavior will be represented by the methods exposed to the users of the class.

Related