13

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.

mohi666
  • 6,842
  • 9
  • 45
  • 51
  • [That wouldn't have worked anyway even without `dataclass`.](https://ideone.com/jG5q9U) – user2357112 Aug 01 '18 at 19:02
  • If it were a regular class, it would have returned something like this, when trying to access m.aaa: – mohi666 Aug 01 '18 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. – mohi666 Aug 01 '18 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.) – user2357112 Aug 01 '18 at 21:10
  • Thank you for providing the clarification. – mohi666 Aug 01 '18 at 21:53

4 Answers4

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)])
mohi666
  • 6,842
  • 9
  • 45
  • 51
  • 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`. – user2357112 Aug 01 '18 at 20:33
  • I just tested this and it works! Thanks. [Code snippet with a working example in python 3.7](https://pastebin.com/JDrkpUg1) – Alechan Jun 04 '20 at 22:07
  • But if the actual (non-test) code uses `fields(MyDataClass)`, this will not work. – cowlinator Dec 15 '22 at 21:57
  • 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'])`. – cowlinator Dec 16 '22 at 00:16
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'>
Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
1

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
Emptyless
  • 2,964
  • 3
  • 20
  • 30
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)
cowlinator
  • 7,195
  • 6
  • 41
  • 61