How to use spec when mocking data classes in Python
Asked Answered
M

4

14

I'm trying to port our namedtuple classes into dataclass in Python 3.6 using the backport package. However, I noticed when mocking dataclass classes, you cannot use the "spec" keyword anymore. I assume it's because the dataclass code is auto generated.

from dataclasses import dataclass
import mock

@dataclass
class A:
    aaa: str
    bbb: int


m = mock.Mock(spec=A)

m.aaa

And this is the error I get:

AttributeError: Mock object has no attribute 'aaa'

Any idea if there's any way to automatically set all the attributes from original object to the mock object? I have lots of data classes with a lot of data. It's going to be really tedious if I try to manually set the values one by one.

Mollymollycoddle answered 1/8, 2018 at 18:57 Comment(5)
That wouldn't have worked anyway even without dataclass.Anlage
If it were a regular class, it would have returned something like this, when trying to access m.aaa: <Mock name='mock.aaa' id='139990326478496'>Mollymollycoddle
Oh, you were trying to access instance variables. In my case I was using class variables, similar to what namedtuple and dataclasse classes are defined.Mollymollycoddle
Class variables also should work pretty much the same with or without dataclass. You just don't have any class variables here. (aaa: str is an annotation, not a class variable, and the dataclass processing doesn't create an aaa class variable.)Anlage
Thank you for providing the clarification.Mollymollycoddle
M
10

I ended up using this generic helper function to achieve what spec does with regular classes:

import mock
from dataclasses import fields


def create_dataclass_mock(obj):
    return mock.Mock(spec=[field.name for field in fields(obj)])
Mollymollycoddle answered 1/8, 2018 at 20:24 Comment(4)
This is no better than just using mock.Mock() directly with no spec. It won't prevent accesses like mock_obj.attribute_that_should_not_exist.Anlage
I just tested this and it works! Thanks. Code snippet with a working example in python 3.7Anny
But if the actual (non-test) code uses fields(MyDataClass), this will not work.Core
Update: to resolve this, I was able to patch dataclasses.fields with return_value=[MockField(name) for name in MyMock._spec_signature.parameters.keys()] where MockField = namedtuple('Field', ['name']).Core
E
6

You can also pass an instance with dummy values to spec

from unittest.mock import Mock
from dataclasses import dataclass

@dataclass
class A:
    aaa: str
    bbb: int

m = Mock(spec=A(None, None))

print(m.bbb)
# <Mock name='mock.bbb' id='139766470904856'>
Elroyels answered 3/8, 2018 at 18:45 Comment(0)
P
2

Based on the answer from mohi666. If you also want to prevent setting Mock attributes not specified in the dataclass, use spec_set instead of spec:

import mock
from dataclasses import dataclass, fields

@dataclass
class A:
   x: str


def create_dataclass_mock(obj):
    return mock.Mock(spec_set=[field.name for field in fields(obj)])

m = create_dataclass_mock(A)
m.x = 'test' # works
m.y = 'another test' # raises AttributeError
Popele answered 1/6, 2023 at 20:25 Comment(0)
C
0

Dataclass fields are actually implemented as instance variables. You have 4 options:

  • give the field a default value:
@dataclass()
class MyDataClass1:
    my_field: str = field(default="my_field_val")
m = Mock(spec=MyDataClass1("my_field_val"))
print(m.my_field)
  • provide the value for the field as an arg in the constructor call of the dataclass:
@dataclass()
class MyDataClass2:
    my_field: str = field()
m = Mock(spec=MyDataClass2("my_field_val"))
print(m.my_field)
  • Instantiate the mock:
@dataclass()
class MyDataClass3:
    my_field: str = field()
m = Mock(spec=MyDataClass3)
m_inst = m()
print(m_inst.my_field)
  • Use a list comprehension to instantiate all fields in the spec:
@dataclass()
class MyDataClass4:
    my_field: str = field()
m = Mock(spec_set=[field.name for field in fields(MyDataClass4)])
print(m.my_field)
Core answered 18/8, 2023 at 23:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.