18

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.

6
  • That wouldn't have worked anyway even without dataclass. Commented Aug 1, 2018 at 19:02
  • 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'> Commented Aug 1, 2018 at 20:06
  • 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. Commented Aug 1, 2018 at 21:06
  • 3
    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.) Commented Aug 1, 2018 at 21:10
  • Thank you for providing the clarification. Commented Aug 1, 2018 at 21:53

4 Answers 4

12

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)])
Sign up to request clarification or add additional context in comments.

4 Comments

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.
I just tested this and it works! Thanks. Code snippet with a working example in python 3.7
But if the actual (non-test) code uses fields(MyDataClass), this will not work.
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']).
7

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'>

Comments

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

Comments

1

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.