From b8f2e96783299dab39a6f6c13992983a07eb14a4 Mon Sep 17 00:00:00 2001 From: Rust Saiargaliev Date: Fri, 27 Oct 2023 16:38:43 +0200 Subject: [PATCH] Fix #47 -- migrate docs to Markdown via MyST --- .readthedocs.yaml | 3 - docs/Makefile | 177 -------------- docs/basic_usage.md | 320 ++++++++++++++++++++++++ docs/{source => }/conf.py | 10 +- docs/how_bakery_behaves.md | 126 ++++++++++ docs/index.md | 45 ++++ docs/make.bat | 242 ------------------- docs/migrating_from_mommy.md | 16 ++ docs/recipes.md | 361 ++++++++++++++++++++++++++++ docs/source/index.md | 45 ++++ docs/source/migrating_from_mommy.md | 16 ++ docs/source/test_runners.md | 53 ++++ docs/test_runners.md | 53 ++++ pyproject.toml | 1 + 14 files changed, 1044 insertions(+), 424 deletions(-) delete mode 100644 docs/Makefile create mode 100644 docs/basic_usage.md rename docs/{source => }/conf.py (71%) create mode 100644 docs/how_bakery_behaves.md create mode 100644 docs/index.md delete mode 100644 docs/make.bat create mode 100644 docs/migrating_from_mommy.md create mode 100644 docs/recipes.md create mode 100644 docs/source/index.md create mode 100644 docs/source/migrating_from_mommy.md create mode 100644 docs/source/test_runners.md create mode 100644 docs/test_runners.md diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 12cd4178..98d9e442 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,6 +11,3 @@ python: path: . extra_requirements: - docs - -sphinx: - configuration: docs/source/conf.py diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 201d55a4..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ModelMommy.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ModelMommy.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/ModelMommy" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ModelMommy" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/basic_usage.md b/docs/basic_usage.md new file mode 100644 index 00000000..1b464841 --- /dev/null +++ b/docs/basic_usage.md @@ -0,0 +1,320 @@ +# Basic Usage + +Let's say you have an app **shop** with a model like this: + +File: **models.py** + +``` +class Customer(models.Model): + """ + Model class Customer of shop app + """ + enjoy_jards_macale = models.BooleanField() + name = models.CharField(max_length=30) + email = models.EmailField() + age = models.IntegerField() + bio = models.TextField() + days_since_last_login = models.BigIntegerField() + birthday = models.DateField() + last_shopping = models.DateTimeField() +``` + +To create a persisted instance, just call Model Bakery: + +File: **test_models.py** + +``` +#Core Django imports +from django.test import TestCase + +#Third-party app imports +from model_bakery import baker + +from shop.models import Customer + +class CustomerTestModel(TestCase): + """ + Class to test the model Customer + """ + + def setUp(self): + self.customer = baker.make(Customer) +``` + +Importing every model over and over again is boring. So let Model Bakery import them for you: + +``` +from model_bakery import baker + +# 1st form: app_label.model_name +customer = baker.make('shop.Customer') + +# 2nd form: model_name +product = baker.make('Product') +``` + +:::{note} +You can only use the 2nd form on unique model names. If you have an app shop with a Product, and an app stock with a Product, you must use the app_label.model_name form. +::: + +:::{note} +model_name is case insensitive. +::: + +## Model Relationships + +Model Bakery also handles relationships. Let's say the customer has a purchase history: + +File: **models.py** + +``` +class Customer(models.Model): + """ + Model class Customer of shop app + """ + enjoy_jards_macale = models.BooleanField() + name = models.CharField(max_length=30) + email = models.EmailField() + age = models.IntegerField() + bio = models.TextField() + days_since_last_login = models.BigIntegerField() + birthday = models.DateField() + appointment = models.DateTimeField() + +class PurchaseHistory(models.Model): + """ + Model class PurchaseHistory of shop app + """ + customer = models.ForeignKey('Customer') + products = models.ManyToManyField('Product') + year = models.IntegerField() +``` + +You can use Model Bakery as: + +``` +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.history = baker.make('shop.PurchaseHistory') + print(self.history.customer) +``` + +It will also create the Customer, automagically. + +**NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept unpersisted model instances anymore. This means that if you run: + +``` +baker.prepare('shop.PurchaseHistory') +``` + +You'll end up with a persisted "Customer" instance. + +## M2M Relationships + +By default Model Bakery doesn't create related instances for many-to-many relationships. If you want them to be created, you have to turn it on as following: + +``` +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.history = baker.make('shop.PurchaseHistory', make_m2m=True) + print(self.history.products.count()) +``` + +## Explicit M2M Relationships + +If you want to, you can prepare your own set of related object and pass it to Model Bakery. Here's an example: + +``` +products_set = baker.prepare(Product, _quantity=5) +history = baker.make(PurchaseHistory, products=products_set) +``` + +## Explicit values for fields + +By default, Model Bakery uses random values to populate the model's fields. But it's possible to explicitly set values for them as well. + +``` +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21 + ) + + self.older_customer = baker.make( + 'shop.Customer', + age=42 + ) +``` + +You can use callable to explicitly set values as: + +``` +import random + +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + def get_random_name(self): + return random.choice(["Suraj Magdum", "Avadhut More", "Rohit Chile"]) + + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21, + name = self.get_random_name + ) +``` + +You can also use iterable to explicitly set values as: + +``` +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + def setUp(self): + names = ("Onkar Awale", "Pruthviraj Patil", "Shubham Ojha") + + self.customer = baker.make( + 'shop.Customer', + age=21, + name = itertools.cycle(names) + ) +``` + +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` + +``` +from django.test import TestCase + +from model_bakery import baker + +from model_bakery.recipe import seq + +class CustomerTestModel(TestCase): + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21, + name = seq('Joe') + ) +``` + +Related objects fields are also reachable by their name or related names in a very similar way as Django does with [field lookups](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups): + +``` +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.bob_history = baker.make( + 'shop.PurchaseHistory', + customer__name='Bob' + ) +``` + +## Creating Files + +Model Bakery does not create files for FileField types. If you need to have the files created, you can pass the flag `_create_files=True` (defaults to `False`) to either `baker.make` or `baker.make_recipe`. + +**Important**: the lib does not do any kind of file clean up, so it's up to you to delete the files created by it. + +## Non persistent objects + +If you don't need a persisted object, Model Bakery can handle this for you as well with the **prepare** method: + +```python +from model_bakery import baker + +customer = baker.prepare('shop.Customer') +``` + +It works like `make` method, but it doesn't persist the instance neither the related instances. + +If you want to persist only the related instances but not your model, you can use the `_save_related` parameter for it: + +```python +from model_bakery import baker + +history = baker.prepare('shop.PurchaseHistory', _save_related=True) +assert history.id is None +assert bool(history.customer.id) is True +``` + +## More than one instance + +If you need to create more than one instance of the model, you can use the `_quantity` parameter for it: + +```python +from model_bakery import baker + +customers = baker.make('shop.Customer', _quantity=3) +assert len(customers) == 3 +``` + +It also works with `prepare`: + +```python +from model_bakery import baker + +customers = baker.prepare('shop.Customer', _quantity=3) +assert len(customers) == 3 +``` + +The `make` method also accepts a parameter `_bulk_create` to use Django's [bulk_create](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#bulk-create) method instead of calling `obj.save()` for each created instance. + +:::{note} +Django's `bulk_create` does not update the created object primary key as explained in their docs. Because of that, there's no way for model-bakery to avoid calling `save` method for all the foreign keys. But this behavior can depends on which Django version and database backend you're using. + +So, for example, if you're trying to create 20 instances of a model with a foreign key using `_bulk_create` this will result in 21 queries (20 for each foreign key object and one to bulk create your 20 instances). +::: + +If you want to avoid that, you'll have to perform individual bulk creations per foreign keys as the following example: + +```python +from model_bakery import baker + +baker.prepare(User, _quantity=5, _bulk_create=True) +user_iter = User.objects.all().iterator() +baker.prepare(Profile, user=user_iter, _quantity=5, _bulk_create=True) +``` + +## Multi-database support + +Model Bakery supports django application with more than one database. +If you want to determine which database bakery should use, you have the `_using` parameter: + +```python +from model_bakery import baker + +custom_db = "your_custom_db" +assert custom_db in settings.DATABASES +history = baker.make('shop.PurchaseHistory', _using=custom_db) +assert history in PurchaseHistory.objects.using(custom_db).all() +assert history.customer in Customer.objects.using(custom_db).all() +# default database tables with no data +assert not PurchaseHistory.objects.exists() +assert not Customer.objects.exists() +``` diff --git a/docs/source/conf.py b/docs/conf.py similarity index 71% rename from docs/source/conf.py rename to docs/conf.py index 638dd563..924e1ede 100644 --- a/docs/source/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ import os import sys -sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath("..")) from model_bakery import __about__ # noqa @@ -10,7 +10,13 @@ author = "Rust Saiargaliev" version = release = __about__.__version__ -extensions = [] +extensions = [ + "myst_parser", +] + +myst_enable_extensions = [ + "colon_fence", +] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] diff --git a/docs/how_bakery_behaves.md b/docs/how_bakery_behaves.md new file mode 100644 index 00000000..fc7fc5aa --- /dev/null +++ b/docs/how_bakery_behaves.md @@ -0,0 +1,126 @@ +# How Model Bakery behaves? + +By default, Model Bakery skips fields with `null=True` or `blank=True`. Also if a field has a `default` value, it will be used. + +You can override this behavior by: + +1. Explicitly defining values + +```python +# from "Basic Usage" page, assume all fields either null=True or blank=True +from model_bakery import baker + +customer = baker.make('shop.Customer', enjoy_jards_macale=True, bio="A fan of Jards Malacé") +``` + +2. Passing `_fill_optional` with a list of fields to fill with random data + +```python +customer = baker.make('shop.Customer', _fill_optional=['enjoy_jards_macale', 'bio']) +``` + +3. Passing `_fill_optional=True` to fill all fields with random data + +```python +customer = baker.make('shop.Customer', _fill_optional=True) +``` + +## When shouldn't you let Baker generate things for you? + +If you have fields with special validation, you should set their values by yourself. + +Model Bakery should handle fields that: + +1. don't matter for the test you're writing; +2. don't require special validation (like unique, etc); +3. are required to create the object. + +## Currently supported fields + +- `BooleanField`, `NullBooleanField`, `IntegerField`, `BigIntegerField`, `SmallIntegerField`, `PositiveIntegerField`, `PositiveSmallIntegerField`, `FloatField`, `DecimalField` +- `CharField`, `TextField`, `BinaryField`, `SlugField`, `URLField`, `EmailField`, `IPAddressField`, `GenericIPAddressField`, `ContentType` +- `ForeignKey`, `OneToOneField`, `ManyToManyField` (even with through model) +- `DateField`, `DateTimeField`, `TimeField`, `DurationField` +- `FileField`, `ImageField` +- `JSONField`, `ArrayField`, `HStoreField` +- `CICharField`, `CIEmailField`, `CITextField` +- `DecimalRangeField`, `IntegerRangeField`, `BigIntegerRangeField`, `DateRangeField`, `DateTimeRangeField` + +Require `django.contrib.gis` in `INSTALLED_APPS`: + +- `GeometryField`, `PointField`, `LineStringField`, `PolygonField`, `MultiPointField`, `MultiLineStringField`, `MultiPolygonField`, `GeometryCollectionField` + +## Custom fields + +Model Bakery allows you to define generators methods for your custom fields or overrides its default generators. +This can be achieved by specifying the field and generator function for the `generators.add` function. +Both can be the real python objects imported in settings or just specified as import path string. + +Examples: + +```python +from model_bakery import baker + +def gen_func(): + return 'value' + +baker.generators.add('test.generic.fields.CustomField', gen_func) +``` + +```python +# in the module code.path: +def gen_func(): + return 'value' + +# in your tests.py file: +from model_bakery import baker + +baker.generators.add('test.generic.fields.CustomField', 'code.path.gen_func') +``` + +## Customizing Baker + +In some rare cases, you might need to customize the way Baker base class behaves. +This can be achieved by creating a new class and specifying it in your settings files. It is likely that you will want to extend Baker, however the minimum requirement is that the custom class have `make` and `prepare` functions. +In order for the custom class to be used, make sure to use the `model_bakery.baker.make` and `model_bakery.baker.prepare` functions, and not `model_bakery.baker.Baker` directly. + +Examples: + +```python +# in the module code.path: +class CustomBaker(baker.Baker): + def get_fields(self): + return [ + field + for field in super(CustomBaker, self).get_fields() + if not field isinstance CustomField + ] + +# in your settings.py file: +BAKER_CUSTOM_CLASS = 'code.path.CustomBaker' +``` + +Additionally, if you want to your created instance to be returned respecting one of your custom ModelManagers, you can use the `_from_manager` parameter as the example below: + +```python +movie = baker.make(Movie, title='Old Boys', _from_manager='availables') # This will use the Movie.availables model manager +``` + +## Save method custom parameters + +If you have overwritten the `save` method for a model, you can pass custom parameters to it using Model Bakery. Example: + +```python +class ProjectWithCustomSave(models.Model) + # some model fields + created_by = models.ForeignKey(settings.AUTH_USER_MODEL) + + def save(self, user, *args, **kwargs): + self.created_by = user + return super(ProjectWithCustomSave, self).save(*args, **kwargs) + +#with model baker: +user = baker.make(settings.AUTH_USER_MODEL) +project = baker.make(ProjectWithCustomSave, _save_kwargs={'user': user}) +assert user == project.user +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..3312422c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +# Model Bakery: Smart fixtures for better tests + +Model Bakery offers you a smart way to create fixtures for testing in Django. + +With a simple and powerful API, you can create many objects with a single line of code. + +Model Bakery is a rename of the legacy [model_mommy\'s project](https://pypi.org/project/model_mommy/). This is because the project\'s creator and maintainers decided to not reinforce gender stereotypes for women in technology. You can read more about this subject[here](https://witi.com/articles/1017/How-Gender-Stereotypes-are-Still-Affecting-Women-in-Tech/). + +# Compatibility + +Model Bakery supports Django \>= 3.2. + +# Install + +Install it with `pip` + +```console +$ pip install model-bakery +``` + +# Contributing to Model Bakery + +As an open-source project, Model Bakery welcomes contributions of many forms. Examples of contributions include: + +- Code Patches +- Documentation improvements +- Bug reports + +Take a look in our [GitHub repo](https://github.com/model-bakers/model_bakery/blob/main/CONTRIBUTING.md) for more instructions on how to set up your local environment to help Model Bakery to grow. + +# Doubts? Loved it? Hated it? Suggestions? + +Feel free to [open an issue](https://github.com/model-bakers/model_bakery/issues/new) for support, development or ideas! + +## Contents + +```{toctree} +:maxdepth: 4 + +basic_usage +recipes +how_bakery_behaves +test_runners +migrating_from_mommy +``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2c346bf7..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ModelMommy.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ModelMommy.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/migrating_from_mommy.md b/docs/migrating_from_mommy.md new file mode 100644 index 00000000..5a542fbf --- /dev/null +++ b/docs/migrating_from_mommy.md @@ -0,0 +1,16 @@ +# Migrating from Model Mommy + +Model Bakery has a [Python script](https://github.com/model-bakers/model_bakery/blob/main/utils/from_mommy_to_bakery.py) to help you to migrate your project\'s test code from Model Mommy to Model Bakery. This script will rename recipe files and replace legacy imports by the new ones. + +**From your project\'s root dir**, execute the following commands: + +```console +$ pip uninstall model_mommy +$ pip install model_bakery +$ wget https://raw.githubusercontent.com/model-bakers/model_bakery/main/utils/from_mommy_to_bakery.py +$ python from_mommy_to_bakery.py --dry-run # will list the files that'll be changed +$ python from_mommy_to_bakery.py # migrate from model_mommy to model_bakery +$ python manage.py test +``` + +This command will only migrate `*.py` files. Any other file type such as `tox.ini`, `requirements.txt` etc, have to be updated manually. diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 00000000..4ea2b6e5 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,361 @@ +# 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** + +``` +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** + +``` +from django.test import TestCase + +from model_bakery import baker + +from shop.models import Customer, Contact + +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: + +``` +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** + +``` +company_recipe = Recipe(Company, name='WidgetCo') +``` + +File: **test_model.py** + +``` +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 + +>>> customer = Recipe(Customer, + name=seq('Joe'), + age=seq(15) +) + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.name +'Joe1' +>>> customer.age +16 + +>>> new_customer = baker.make_recipe('shop.customer') +>>> 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 + +>>> customer = Recipe(Customer, email=seq('user', suffix='@example.com')) + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.email +'user1@example.com' + +>>> customer = baker.make_recipe('shop.customer') +>>> 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 + + +>>> customer = 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.customer') +>>> 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.customer') +>>> 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, +) +``` diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 00000000..581bb096 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,45 @@ +# Model Bakery: Smart fixtures for better tests + +Model Bakery offers you a smart way to create fixtures for testing in Django. + +With a simple and powerful API you can create many objects with a single line of code. + +Model Bakery is a rename of the legacy [model_mommy's project](https://pypi.org/project/model_mommy/). This is because the project's creator and maintainers decided to not reinforce gender stereotypes for women in technology. You can read more about this subject [here](https://witi.com/articles/1017/How-Gender-Stereotypes-are-Still-Affecting-Women-in-Tech/). + +# Compatibility + +Model Bakery supports Django >= 3.2. + +# Install + +Install it with `pip` + +```console +$ pip install model-bakery +``` + +# Contributing to Model Bakery + +As an open source project, Model Bakery welcomes contributions of many forms. Examples of contributions include: + +- Code Patches +- Documentation improvements +- Bug reports + +Take a look in our [Github repo](https://github.com/model-bakers/model_bakery/blob/main/CONTRIBUTING.md) for more instructions on how to set up your local environment to help Model Bakery to grow. + +# Doubts? Loved it? Hated it? Suggestions? + +Feel free to [open an issue](https://github.com/model-bakers/model_bakery/issues/new) for support, development or ideas! + +## Contents + +```{toctree} +:maxdepth: 4 + +basic_usage +recipes +how_bakery_behaves +test_runners +migrating_from_mommy +``` diff --git a/docs/source/migrating_from_mommy.md b/docs/source/migrating_from_mommy.md new file mode 100644 index 00000000..1d6b7039 --- /dev/null +++ b/docs/source/migrating_from_mommy.md @@ -0,0 +1,16 @@ +# Migrating from Model Mommy + +Model Bakery has a [Python script](https://github.com/model-bakers/model_bakery/blob/main/utils/from_mommy_to_bakery.py) to help you to migrate your project's test code from Model Mommy to Model Bakery. This script will rename recipe files and replace legacy imports by the new ones. + +**From your project's root dir**, execute the following commands: + +```console +$ pip uninstall model_mommy +$ pip install model_bakery +$ wget https://raw.githubusercontent.com/model-bakers/model_bakery/main/utils/from_mommy_to_bakery.py +$ python from_mommy_to_bakery.py --dry-run # will list the files that'll be changed +$ python from_mommy_to_bakery.py # migrate from model_mommy to model_bakery +$ python manage.py test +``` + +This command will only migrate `*.py` files. Any other file type such as `tox.ini`, `requirements.txt` etc, have to be updated manually. diff --git a/docs/source/test_runners.md b/docs/source/test_runners.md new file mode 100644 index 00000000..b24ebb21 --- /dev/null +++ b/docs/source/test_runners.md @@ -0,0 +1,53 @@ +# Test Runners + +Most of the code examples shown so far have used the [Django TestCase](https://docs.djangoproject.com/en/3.1/topics/testing/tools/#testcase) to explain how Model Bakery is used. + +However [pytest](https://docs.pytest.org/en/stable/) (with the [pytest-django](https://pytest-django.readthedocs.io/en/latest/) plugin) is often preferred for it's simplicity and other benefits. See [here](https://realpython.com/django-pytest-fixtures/). + +The following examples show Model Bakery usage with different test runners. + +## Django + +``` +# Core Django imports +from django.test import TestCase + +# Third-party app imports +from model_bakery import baker + +from shop.models import Customer + +class CustomerTestModel(TestCase): + """ + Class to test the model Customer + """ + + def setUp(self): + """Set up test class.""" + self.customer = baker.make(Customer) + + def test_using_customer(self): + """Test function using baked model.""" + self.assertIsInstance(self.customer, Customer) +``` + +## pytest + +``` +# pytest import +import pytest + +# Third-party app imports +from model_bakery import baker + +from shop.models import Customer + +@pytest.fixture +def customer(): + """Fixture for baked Customer model.""" + return baker.make(Customer) + +def test_using_customer(customer): + """Test function using fixture of baked model.""" + assert isinstance(customer, Customer) +``` diff --git a/docs/test_runners.md b/docs/test_runners.md new file mode 100644 index 00000000..147abef5 --- /dev/null +++ b/docs/test_runners.md @@ -0,0 +1,53 @@ +# Test Runners + +Most of the code examples shown so far have used the [Django TestCase](https://docs.djangoproject.com/en/dev/topics/testing/tools/#testcase) to explain how Model Bakery is used. + +However, [pytest](https://docs.pytest.org/en/stable/) (with the [pytest-django](https://pytest-django.readthedocs.io/en/latest/) plugin) is often preferred for it\'s simplicity and other benefits. See [here](https://realpython.com/django-pytest-fixtures/). + +The following examples show Model Bakery usage with different test runners. + +## Django + +```python + # Core Django imports + from django.test import TestCase + + # Third-party app imports + from model_bakery import baker + + from shop.models import Customer + + class CustomerTestModel(TestCase): + """ + Class to test the model Customer + """ + + def setUp(self): + """Set up test class.""" + self.customer = baker.make(Customer) + + def test_using_customer(self): + """Test function using baked model.""" + self.assertIsInstance(self.customer, Customer) +``` + +## pytest + +```python + # pytest import + import pytest + + # Third-party app imports + from model_bakery import baker + + from shop.models import Customer + + @pytest.fixture + def customer(): + """Fixture for baked Customer model.""" + return baker.make(Customer) + + def test_using_customer(customer): + """Test function using fixture of baked model.""" + assert isinstance(customer, Customer) +``` diff --git a/pyproject.toml b/pyproject.toml index 957cf9bc..3d651e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ test = [ docs = [ "Sphinx", "sphinx-rtd-theme", + "myst-parser", ] [project.urls]