2

I'm working on a Python project and wanted to cover my code with unit tests. For each test I create a test class with multiple methods, and define initial testing conditions inside a method. Specifically, I have a class Node which I test in Test1 class, and all tests work as expected. But then, I want to test a different segment of code. For that purpose I create new class Test2 and create an instance of Node class inside a method of Test2. When I create it I expect the object to have default attributes that initialized with __init__(), however it has parameters initialized in Test1. I'm not sure why the objects have shared attributes even though I do not explicitly share them.

Below is my project structure:

package_with_tests
|- objects
   |- __init__.py
   |- node.py
   |- time_series.py
|- tests
   |- t_objects
      |- __init__.py
      |- t_func.py
      |- t_node.py
   |- __init__.py
   |- tests_collect.py
|- __init__.py

Here is a time_series.py code

import numpy as np


class TimeSeries:
    _ts = np.array([])
    _ys = np.array([])

    def __init__(self, ts: (np.ndarray, list), ys: (np.ndarray, list)):

        if isinstance(ts, list):
            ts = np.array(ts)

        if isinstance(ys, list):
            ys = np.array(ys)

        self.ts = ts
        self.ys = ys

    @property
    def ts(self) -> np.ndarray:
        return self._ts

    @ts.setter
    def ts(self, val: np.ndarray):
        self._ts = val

    @property
    def ys(self) -> np.ndarray:
        return self._ys

    @ys.setter
    def ys(self, val: np.ndarray):
        self._ys = val

    @property
    def size(self) -> int:
        return self._ts.shape[0]

    def append(self, ts, ys):
        self._ts = np.append(self._ts, ts)
        self._ys = np.append(self._ys, ys)

node.py code:

from package_with_tests.objects.time_series import TimeSeries


class Node:
    _attribute_1 = TimeSeries([], [])
    _rt_attribute = (0, 0)

    def __init__(self):
        pass

    @property
    def attribute_1(self) -> TimeSeries:
        return self._attribute_1

    @attribute_1.setter
    def attribute_1(self, val: TimeSeries):
        self._attribute_1 = val

    @property
    def rt_attribute(self) -> tuple:
        return self._rt_attribute

    @rt_attribute.setter
    def rt_attribute(self, val: tuple):
        self._rt_attribute = val

        ts, ys = val

        self._attribute_1.append(ts, ys)

tests.t_objects.t_node.py code

from unittest import TestCase

from package_with_tests.objects.node import Node


class Test1(TestCase):
    def test_1_1(self):
        node = Node()

        self.assertEqual(node.attribute_1.size, 0)

        # Note that I append new value to time_series if rt_attribute get updated
        node.rt_attribute = (10, 20)
        self.assertEqual(node.attribute_1.size, 1)
        self.assertEqual(node.attribute_1.ts[0], 10)

tests.t_objects.t_func.py code:

from unittest import TestCase

from package_with_tests.objects.node import Node


class Test2(TestCase):
    def test_2_1(self):
        # Here I create a new Node object and I would expect it to come with 
        # default attributes, i.e. 
        #    _attribute_1 = TimeSeries([], [])
        #    _rt_attribute = (0, 0)
        # But, get attribute_1 defined in Test1.test_1_1()
        node404 = Node()

        self.assertEqual(node404.attribute_1.size, 0,
                         msg=f"size={node404.attribute_1.size}, "
                             f"ts={node404.attribute_1.ts}, "
                             f"ys={node404.attribute_1.ys}")

And code of tests_collect.py

import unittest

from package_with_tests.tests.t_objects.t_node import Test1
from package_with_tests.tests.t_objects.t_func import Test2


if __name__ == "__main__":
    loader = unittest.TestLoader()
    test_suite = unittest.TestSuite()

    test_suite.addTest(loader.loadTestsFromTestCase(Test1))
    test_suite.addTest(loader.loadTestsFromTestCase(Test2))

    runner = unittest.TextTestRunner()
    runner.run(test_suite)

I run all the tests with command

$ python -m package_with_tests.tests.tests_collect

(Python version is 3.7.6) and get the following output

======================================================================
FAIL: test_2_1 (package_with_tests.tests.t_objects.t_func.Test2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/username/Projects/TestProject/package_with_tests/tests/t_objects/t_func.py", line 11, in test_2_1
    msg=f"size={node404.attribute_1.size}, "
AssertionError: 1 != 0 : size=1, ts=[10.], ys=[20.]

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

As you can see from row AssertionError: 1 != 0 : size=1, ts=[10.], ys=[20.] node.attribute_1 has ts equal to [10.] and ys equal to [20.] that were defined in Test1.test_1_1().

However if I switch the order of how tests added to the test suite in the tests_collect.py

    ...
    test_suite.addTest(loader.loadTestsFromTestCase(Test2))
    test_suite.addTest(loader.loadTestsFromTestCase(Test1))
    ...

all tests pass without failures.

I do not understand why the object initialized in Test2 has attributes of the object initialized in Test1. They get shared somehow, but I wasn't able to find an answer as of why this is happening. I tried to delete object after the test is done with

del node

with no success.

I searched through SO and found a very similar question (Why does the second test method appear to share the context of the first test method?) however there are no answers yet. Any help would be really appreciated

Rafid Aslam
  • 305
  • 3
  • 10
  • [This](https://stackoverflow.com/questions/1680528/how-to-avoid-having-class-data-shared-among-instances) answer indeed helped. After I added explicit initialization of all attributes to `Node.__init__()` the issue has gone. – Alexey Lukyanov Jan 06 '21 at 14:33

0 Answers0