Why does the second test method appear to share the context of the first test method?

北慕城南 提交于 2021-02-11 12:40:22

问题


I'm absolutely new to unit-testing, so forgive me if that question is either too vague or appears to be off the mark. The problem is that I'm at a loss in identifying the source of this issue. I'm unsure if the problem is with my limited understanding of unit-testing or if I have not considered another core Python concept. The questing reflects the conclusion that I have come to at this point. Allow me to explanation to you my situation.

I'm building a tic tac toe game, which I set out to do to practice using Python classes. I have a couple of classes, one for the Board, and the other for each Box in the Board.

class Box():
    all_boxes = []
    def __init__(self, on_left=None, on_right=None, on_top=None, on_bottom=None, mark=' '):
        self.on_left = on_left
        self.on_right = on_right
        self.on_top = on_top
        self.on_bottom = on_bottom
        self.mark = mark
        self.all_boxes.append(self)
        logging.debug(self.all_boxes)
    def set_attributes(self, **kwargs):
        '''This method is used to set the attributes of a Box class object, by passing them as keyword arguments.'''
        for k, v in kwargs.items():
            setattr(self, k, v)
    def is_row_complete(self):
        '''This method returns True if the row containing the box is completed by one player.'''
        current_box = self
        ordered_row = [current_box.mark]
        while True:
            if current_box.on_left:
                ordered_row.insert(0, current_box.on_left.mark)
                current_box = current_box.on_left
            else:
                current_box = self
                break
        while True:
            if current_box.on_right:
                ordered_row.append(current_box.on_right.mark)
                current_box = current_box.on_right
            else:
                break
        
        if ordered_row.count('X') == 3 or ordered_row.count('O') == 3:
            return True
    def is_column_complete(self):
        '''This method returns True if the column containing the box is completed by one player.'''
        current_box = self
        ordered_column = [current_box.mark]
        while True:
            if current_box.on_top:
                ordered_column.insert(0, current_box.on_top.mark)
                current_box = current_box.on_top
            else:
                current_box = self
                break
        while True:
            if current_box.on_bottom:
                ordered_column.append(current_box.on_bottom.mark)
                current_box = current_box.on_bottom
            else:
                break
        
        if ordered_column.count('X') == 3 or ordered_column.count('O') == 3:
            return True
    def is_diagonal_complete(self):
        '''This method returns True if either one of the diagonals is completed by one player.'''
        if self.on_left and self.on_right and self.on_top and self.on_bottom:
            ordered_diagonal_1 = [self.on_left.on_top.mark, self.mark, self.on_right.on_bottom.mark]
            ordered_diagonal_2 = [self.on_left.on_bottom.mark, self.mark, self.on_right.on_top.mark]
            if ordered_diagonal_1.count('X') == 3 or ordered_diagonal_1.count('O') == 3:
                return True
            elif ordered_diagonal_2.count('X') == 3 or ordered_diagonal_2.count('O') == 3:
                return True
    def __repr__(self):
        return 'Box(on_left={}, on_right={}, on_top={}, on_bottom={}, mark={})'.format(
            str(self.on_left),
            str(self.on_right),
            str(self.on_top),
            str(self.on_bottom),
            repr(self.mark)
        )
class Board:
    def __init__(self):
        self.top_left = Box()
        self.top_center = Box()
        self.top_right = Box()
        self.middle_left = Box()
        self.middle_center = Box()
        self.middle_right = Box()
        self.bottom_left = Box()
        self.bottom_center = Box()
        self.bottom_right = Box()
        self.top_left.set_attributes(on_right = self.top_center, on_bottom = self.middle_left)
        self.top_center.set_attributes(on_left = self.top_left, on_right = self.top_right, on_bottom = self.middle_center)
        self.top_right.set_attributes(on_left = self.top_center, on_bottom = self.middle_right)
        self.middle_left.set_attributes(on_right = self.middle_center, on_top = self.top_left, on_bottom = self.bottom_left)
        self.middle_center.set_attributes(on_left = self.middle_left, on_right = self.middle_right, on_top = self.top_center, on_bottom = self.bottom_center)
        self.middle_right.set_attributes(on_left = self.middle_center, on_top = self.top_right, on_bottom = self.bottom_right)
        self.bottom_left.set_attributes(on_right = self.bottom_center, on_top = self.middle_left)
        self.bottom_center.set_attributes(on_left = self.bottom_left, on_right = self.bottom_right, on_top = self.middle_center)
        self.bottom_right.set_attributes(on_left = self.bottom_center, on_top = self.middle_right)
        
        self.available_boxes = ['top_left', 'top_center', 'top_right', 'middle_left', 'middle_center', 'middle_right', 'bottom_left', 'bottom_center', 'bottom_right']
    def user_move(self, move_count):
        '''This method requests user input and executes the user's move.'''
        logging.debug("Scope: Board class' user_move() method")
        if move_count < 2:
            print('Please make your move...\n\nRespond with a combination of Top/Middle/Bottom\nand Left/Center/Right, separated with a hyphen.\n\nFor instance, "Top-Right"/"Bottom-Center"\n')
        while True:
            player_move = input('Your Move: ').lower().replace('-', '_')
            if hasattr(self, player_move):
                selected_box = getattr(self, player_move)
                selected_box.mark = 'X'
                self.available_boxes.remove(player_move)
                self.print_board()
                return
            else:
                print('\nTry "Top-Left", for instance.\n')
                continue
    def computer_move(self):
        '''This method executes the computer's move.'''
        logging.debug("Scope: Board class' computer_move() method")
        print("Machine's Move: ")
        machine_move = self.available_boxes[random.randint(0, len(self.available_boxes) - 1)]
        selected_box = getattr(self, machine_move)
        selected_box.mark = 'O'
        self.available_boxes.remove(machine_move)
        self.print_board()
    def are_spaces_filled(self):
        '''This method checks to see if every space in the board is filled.'''
        if len(self.available_boxes) == 0:
            return True
        else:
            return False
    def find_winner(self):
        '''This method runs through each box, to see if either the row, column or diagonal containing it is complete 
        and returns the mark of the winner.'''
        for box in Box.all_boxes:
            if box.is_row_complete():
                return box.mark
            elif box.is_column_complete():
                return box.mark
            elif box.is_diagonal_complete():
                return box.mark
    def print_board(self):
        '''This method displays a visual representation of the board in the console.'''
        print('\n{} | {} | {}'.format(self.top_left.mark, self.top_center.mark, self.top_right.mark))
        print(' ------- ')
        print('{} | {} | {}'.format(self.middle_left.mark, self.middle_center.mark, self.middle_right.mark))
        print(' ------- ')
        print('{} | {} | {}\n'.format(self.bottom_left.mark, self.bottom_center.mark, self.bottom_right.mark))

The test that I am writing is for the find_winner() method of the Board class. In order to test if the method returns the appropriate response in the case of a diagonal being filled with X's, I manually assign the value 'X' to the 'mark' attribute of the three boxes that represent the top-left to bottom-right diagonal.

import unittest
from tictactoe import Box, Board
class TestTicTacToe(unittest.TestCase):
    def test_diagonal_X(self):
        sample_board = Board()
        sample_board.top_left.mark = 'X'
        sample_board.middle_center.mark = 'X'
        sample_board.bottom_right.mark = 'X'
        
        sample_board.print_board()
self.assertEqual(sample_board.find_winner(), 'X', "Should be 'X'.")
if __name__ == "__main__":
    unittest.main()

The test passes and I'm feeling confident with unit-tests. I add a second test to test the other diagonal with three O's.

def test_diagonal_O(self):
    sample_board = Board()
    sample_board.top_right.mark = 'O'
    sample_board.middle_center.mark = 'O'
    sample_board.bottom_left.mark = 'O'
        
    sample_board.print_board()

    self.assertEqual(sample_board.find_winner(), 'O', "Should be 'O'.")

Excellent, the test with three O's passes. All of a sudden however, the test with the three X's fails. The weird part is that the failure message indicates that the function returns an O when it should return an X. I can't understand how the return value can be an O when not a single box is filled with O. I go through my code manually to see if there is any issue with the code, but there is no way a Board instance can return an O in such a scenario.

I delete the second test and run the test again, and this time the test passes again. So why does the test not pass with the addition of a second test..? I also notice that the order of tests happens with the test with O's running first, and that with the X's running second.

  |   | O
 -------
  | O |
 -------
O |   |

.
X |   |
 -------
  | X |
 -------
  |   | X

F
======================================================================
FAIL: test_diagonal_X (__main__.TestTicTacToe)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "d:/Dev/TicTacToe/Source/test_tictactoe.py", line 13, in test_diagonal_X
    self.assertEqual(self.sample_board.find_winner(), 'X', "Should be 'X'.")
AssertionError: 'O' != 'X'
- O
+ X
 : Should be 'X'.

----------------------------------------------------------------------
Ran 2 tests in 0.026s

FAILED (failures=1)

I wonder if the test for some reason shares context with the other test. I change the names of the Board instances in both the tests. Instead of both sharing the same name, I name them sample_board_1 and sample_board_2. The issue remains.

I go through some online resources and I learn about the setUp() and tearDown() methods. I use the setUp() method to create fresh instances of the sample_board for each test. This doesn't solve the issue either. I use the tearDown() method to delete the instance at the end of each test. No change. I instead use the tearDown() method to set the sample_board variable's value to None at the end of each test. No change.

Finally, I use the tearDown() method to assign the top-right to bottom-left boxes - which hold O's - to ' '. In this case. Both the tests pass. I'm guessing the issue will not repeat if I use the tearDown() method to reassign the blank string value to each box at the end of each test, but I don't understand why this issue happens in the first place.

def setUp(self):
     self.sample_board = Board()

def tearDown(self):
     # FIRST ATTEMPT
     # del self.sample_board

     # SECOND ATTEMPT
     # self.sample_board = None

     # FINAL ATTEMPT
     self.sample_board.top_right.mark = ' '
     self.sample_board.middle_center.mark = ' '
     self.sample_board.bottom_left.mark = ' '
     # pass

Shouldn't the variables held in each method only exist within the scope of the method..? Also, even if the context is shared by the method's, why didn't the attempts to delete the instance, or assign the variable holding the instance a None value work..?

Please help me understand where I'm failing to understand the way things work. Also, if there is a better way to use the unittest module to do what I'm trying to accomplish, please feel free to comment.

来源:https://stackoverflow.com/questions/64630925/why-does-the-second-test-method-appear-to-share-the-context-of-the-first-test-me

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!