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,
)
```
|