File: recipes.md

package info (click to toggle)
python-model-bakery 1.20.5-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 532 kB
  • sloc: python: 4,298; sh: 149; makefile: 21
file content (344 lines) | stat: -rw-r--r-- 10,016 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# Recipes

If you're not comfortable with random data or even if you just want to
improve the semantics of the generated data, there's hope for you.

You can define a **Recipe**, which is a set of rules to generate data
for your models.

It's also possible to store the Recipes in a module called *baker_recipes.py*
at your app's root directory. This recipes can later be used with the
`make_recipe` function:

```
shop/
  migrations/
  __init__.py
  admin.py
  apps.py
  baker_recipes.py   <--- where you should place your Recipes
  models.py
  tests.py
  views.py
```

File: **baker_recipes.py**

```python
from model_bakery.recipe import Recipe
from shop.models import Customer

customer_joe = Recipe(
    Customer,
    name='John Doe',
    nickname='joe',
    age=18,
    birthday=date.today(),
    last_shopping=datetime.now()
)
```

```{note}
You don't have to declare all the fields if you don't want to. Omitted fields will be generated automatically.
```

File: **test_model.py**

```python
from django.test import TestCase

from model_bakery import baker

class CustomerTestModel(TestCase):

    def setUp(self):
        # Load the recipe 'customer_joe' from 'shop/baker_recipes.py'
        self.customer_one = baker.make_recipe(
            'shop.customer_joe'
        )
```

Or if you don't want a persisted instance:

```python
from model_bakery import baker

baker.prepare_recipe('shop.customer_joe')
```

```{note}
You don't have to place necessarily your `baker_recipes.py` file inside your app's root directory.
If you have a tests directory within the app, for example, you can add your recipes inside it and still
use `make_recipe`/`prepare_recipe` by adding the tests module to the string you've passed as an argument.
For example: `baker.make_recipe("shop.tests.customer_joe")`

So, short summary, you can place your `baker_recipes.py` **anywhere** you want to and to use it having in mind
you'll only have to simulate an import but obfuscating the `baker_recipes` module from the import string.
```

```{note}
You can use the \_quantity parameter as well if you want to create more than one object from a single recipe.
```

You can define recipes locally to your module or test case as well. This can be useful for cases where a particular set of values may be unique to a particular test case, but used repeatedly there. For example:

File: **baker_recipes.py**

```python
company_recipe = Recipe(Company, name='WidgetCo')
```

File: **test_model.py**

```python
class EmployeeTest(TestCase):
    def setUp(self):
        self.employee_recipe = Recipe(
            Employee,
            name=seq('Employee '),
            company=baker.make_recipe('app.company_recipe')
        )

    def test_employee_list(self):
        self.employee_recipe.make(_quantity=3)
        # test stuff....

    def test_employee_tasks(self):
        employee1 = self.employee_recipe.make()
        task_recipe = Recipe(Task, employee=employee1)
        task_recipe.make(status='done')
        task_recipe.make(due_date=datetime(2014, 1, 1))
        # test stuff....
```

## Recipes with foreign keys

You can define `foreign_key` relations:

```python
from model_bakery.recipe import Recipe, foreign_key
from shop.models import Customer, PurchaseHistory

customer = Recipe(Customer,
    name='John Doe',
    nickname='joe',
    age=18,
    birthday=date.today(),
    appointment=datetime.now()
)

history = Recipe(PurchaseHistory,
    owner=foreign_key(customer)
)
```

Notice that `customer` is a *recipe*.

You may be thinking: "I can put the Customer model instance directly in the owner field". That's not recommended.

Using the `foreign_key` is important for 2 reasons:

- Semantics. You'll know that attribute is a foreign key when you're reading;
- The associated instance will be created only when you call `make_recipe` and not during recipe definition;

You can also use `related`, when you want two or more models to share the same parent:

```python
from model_bakery.recipe import related, Recipe
from shop.models import Customer, PurchaseHistory

history = Recipe(PurchaseHistory)
customer_with_2_histories = Recipe(Customer,
    name='Albert',
    purchasehistory_set=related('history', 'history'),
)
```

Note this will only work when calling `make_recipe` because the related manager requires the objects in the related_set to be persisted. That said, calling `prepare_recipe` the related_set will be empty.

If you want to set m2m relationship you can use `related` as well:

```python
from model_bakery.recipe import related, Recipe

pencil = Recipe(Product, name='Pencil')
pen = Recipe(Product, name='Pen')
history = Recipe(PurchaseHistory)

history_with_prods = history.extend(
    products=related(pencil, pen)
)
```

When creating models based on a `foreign_key` recipe using the `_quantity` argument, only one related model will be created for all new instances.

```python
from model_baker.recipe import foreign_key, Recipe

person = Recipe(Person, name='Albert')
dog = Recipe(Dog, owner=foreign_key(person))

# All dogs share the same owner
dogs = dog.make_recipe(_quantity=2)
assert dogs[0].owner.id == dogs[1].owner.id
```

This will cause an issue if your models use `OneToOneField`. In that case, you can provide `one_to_one=True` to the recipe to make sure every instance created by `_quantity` has a unique id.

```python
from model_baker.recipe import foreign_key, Recipe

person = Recipe(Person, name='Albert')
dog = Recipe(Dog, owner=foreign_key(person, one_to_one=True))

# Each dog has a unique owner
dogs = dog.make_recipe(_quantity=2)
assert dogs[0].owner.id != dogs[1].owner.id
```

## Recipes with callables

It's possible to use `callables` as recipe's attribute value.

```python
from datetime import date
from model_bakery.recipe import Recipe
from shop.models import Customer

customer = Recipe(
    Customer,
    birthday=date.today,
)
```

When you call `make_recipe`, Model Bakery will set the attribute to the value returned by the callable.

## Recipes with iterators

You can also use *iterators* (including *generators*) to provide multiple values to a recipe.

```python
from itertools import cycle

names = ['Ada Lovelace', 'Grace Hopper', 'Ida Rhodes', 'Barbara Liskov']
customer = Recipe(Customer,
    name=cycle(names)
)
```

Model Bakery will use the next value in the *iterator* every time you create a model from the recipe.

## Sequences in recipes

Sometimes, you have a field with an unique value and using `make` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with `seq`

```python
from model_bakery.recipe import Recipe, seq
from shop.models import Customer

CustomerRecipe = Recipe(Customer,
    name=seq('Joe'),
    age=seq(15)
)

customer = baker.make_recipe('shop.CustomerRecipe')
customer.name  # 'Joe1'
customer.age  # 16

new_customer = baker.make_recipe('shop.CustomerRecipe')
new_customer.name  # 'Joe2'
new_customer.age  # 17
```

This will append a counter to strings to avoid uniqueness problems, and it will sum the counter with numerical values.

An optional `suffix` parameter can be supplied to augment the value for cases like generating emails
or other strings with common suffixes.

```python
from model_bakery import.recipe import Recipe, seq
from shop.models import Customer

CustomerRecipe = Recipe(Customer, email=seq('user', suffix='@example.com'))

customer = baker.make_recipe('shop.CustomerRecipe')
customer.email  # 'user1@example.com'

customer = baker.make_recipe('shop.CustomerRecipe')
customer.email  # 'user2@example.com'
```

Sequences and iterables can be used not only for recipes, but with `baker` as well:

```python
from model_bakery import baker

customer = baker.make('Customer', name=baker.seq('Joe'))
customer.name  # 'Joe1'

customers = baker.make('Customer', name=baker.seq('Chad'), _quantity=3)
for customer in customers:
     print(customer.name)
#  'Chad1'
#  'Chad2'
#  'Chad3'
```

You can also provide an optional `increment_by` argument which will modify incrementing behaviour. This can be an integer, float, Decimal or timedelta. If you want to start your increment differently, you can use the `start` argument, only if it's not a sequence for `date`, `datetime` or `time` objects.

```python
from datetime import date, timedelta
from model_bakery.recipe import Recipe, seq
from shop.models import Customer


CustomerRecipe = Recipe(Customer,
    age=seq(15, increment_by=3)
    height_ft=seq(5.5, increment_by=.25)
    # assume today's date is 21/07/2014
    appointment=seq(date(2014, 7, 21), timedelta(days=1)),
    name=seq('Custom num: ', increment_by=2, start=5),
)

customer = baker.make_recipe('shop.CustomerRecipe')
customer.age  # 18
customer.height_ft  # 5.75
customer.appointment  # datetime.date(2014, 7, 22)
customer.name  # 'Custom num: 5'

new_customer = baker.make_recipe('shop.CustomerRecipe')
new_customer.age  # 21
new_customer.height_ft  # 6.0
new_customer.appointment  # datetime.date(2014, 7, 23)
customer.name  # 'Custom num: 7'
```

Be aware that `seq` may query the database to determine when to reset. Therefore, a `SimpleTestCase` test method (which disallows database access) can call `prepare_recipe` on a Recipe with a `seq` once, but not not more than once within a test, even though the record itself is never saved to the database.

## Overriding recipe definitions

Passing values when calling `make_recipe` or `prepare_recipe` will override the recipe rule.

```python
from model_bakery import baker

baker.make_recipe('shop.customer', name='Ada Lovelace')
```

This is useful when you have to create multiple objects and you have some unique field, for instance.

## Recipe inheritance

If you need to reuse and override existent recipe call extend method:

```python
customer = Recipe(
    Customer,
    bio='Some customer bio',
    age=30,
    enjoy_jards_macale=True,
)
sad_customer = customer.extend(
    enjoy_jards_macale=False,
)
```