0

I am testing a class that contains a few methods for handling performance counters. It's a normal Python class:

class S3Db:
    data = {}
    fs = None
    s3_path = None

    def __init__(self, s3_api_key, s3_api_secret, bucket_name, filename, bucket_path='/'):
        if s3_api_key != 'test':
            self.fs = s3fs.S3FileSystem(s3_api_key, secret=s3_api_secret)
            self.s3_path = f's3://{bucket_name}{bucket_path}{filename}'
            self.data = read_json_s3_path(self.fs, self.s3_path)
        pass

    def increment_timed_counter(self, counter_name: str):
        ...

My tests look like this (note the tested object is created again at the beginning of each test):

def test_can_increment_timed_counter():
    s3db = S3Db('test', 'testsecret', 'bucket', 'filename')

    s3db.init_timed_counter('Test Counter 1', 1000)

    val = s3db.increment_timed_counter('Test Counter 1')
    assert val == 1
    assert s3db.get_timed_counter('Test Counter 1') == 1


def test_can_init_timed_counters():
    # NOTE I CREATE A NEW INSTANCE HERE, SHOULD BE BRAND NEW?
    s3db = S3Db('test', 'testsecret', 'bucket', 'filename')

    s3db.init_timed_counter('Test Counter 2', 1000)

    assert s3db.data == {
        'timed_counters': {
            'Test Counter 2': {
                'name': 'Test Counter 2',
                'expiration_ms': 1000,
                'values': []
            }
        }
    }

But the second test fails with:

E            'timed_counters': {'Test Counter 2': {'expiration_ms': 1000,
E                                                'name': 'Test Counter 2',
E         -                                      'values': []}},
E         ?                                                   -
E         +                                      'values': []},
E         +                     'Test Counter 1': {'expiration_ms': 1000,
E         +                                        'name': 'Test Counter 1',
E         +                                        'values': [854055045839803]}},
E           }

Indicating the counter created on the first test remains on the object being tested on the second test.

Why is this happening if I create a new object on the second test? Shouldn't that first object be garbage collected and the second one be a brand new instance of that class?

I'm on Python 3.9

Thank you!

Edy Bourne
  • 5,679
  • 13
  • 53
  • 101
  • 3
    `data = {}` is a class member and therefore shared across all instances. So if you write anything to it in one instance it will also be reflected in another – UnholySheep Jul 21 '23 at 12:42
  • ohhh geez I always forget about this in Python... ok thank you! If you want to move to an answer, I will accept it. I guess that's why we write tests ;) – Edy Bourne Jul 21 '23 at 12:42

1 Answers1

2

The variable declarations at the beginning of your class creates them as class data members which are shared by all instances. Therefore any modifications (e.g.: via a method call on one instance) will also be visible in all other instances, which can cause such bugs.

The fix is to ensure these members are instance members, e.g.:

class S3Db:
    def __init__(self, s3_api_key, s3_api_secret, bucket_name, filename, bucket_path='/'):
        self.data = {}
        self.fs = None
        self.s3_path = None
        if s3_api_key != 'test':
            self.fs = s3fs.S3FileSystem(s3_api_key, secret=s3_api_secret)
            self.s3_path = f's3://{bucket_name}{bucket_path}{filename}'
            self.data = read_json_s3_path(self.fs, self.s3_path)
UnholySheep
  • 3,967
  • 4
  • 19
  • 24