Introduction to writing and using (automated) tests

< / / / / / >
Beginner
Python Framework Testing
V15.0
V 15.0
+/- 24 minutes
Written by Holden Rehg
(0)

Quick scroll

1. Introduction

Welcome to the testing tutorial for Odoo.

What are we going to be dealing with? In this tutorial, we are not going to get into more advanced concepts and ideas around how or what. Those are all things that are debated throughout development communities and that end up being very personal to the types of team or application.

I want to walk through a realistic use-case of a sample module and then show you the process for writing and running some basic tests.

One question you may immediately be thinking is, why should I write tests?

1.1. Testing is usually not new to a programmer

Usually programmers test the code that they've written. It’s something that we most of us are very accustomed to. But many Odoo developers are not taking full advantage of the automated testing tools provided to us by the framework and by the python language.

1.2. Build your own safety net

Writing tests has benefits for most teams, with each team benefiting in different ways. For me, the most important factor is actually not quantifiable. It makes me a less stressed and happier programmer because I know that I’m slowly sowing my own safety net for a project. As each major feature or use case gets a test confirming how it’s expected to work, then that protects me from accidentally deploying broken code. We can’t pull up a large application and manually test all aspects every deployment. Also, as each bug arises a test can be added to confirm that the bug has been resolved and again prevents us from regressing and deploying broken code. In this case, the work kind of broken code being a bug that the client has asked you to fix already.

1.3. Start simple

There are many, many types of tests floating around out there. Then there are also multiple names per type of tests depending on who you are talking to. Integration, unit, functional, etc. You can dig into all of these down the road as you get further into testing. Just start writing tests that replicate the same steps you would take manually.


So I’m going to try to give you all of the tools that you need to get started with testing. In my experience, testing is not as prolific within the Odoo community compared to others, so I’m hoping that this gives a kick start to Odoo developers out there!

2. Building a sample module to test

If you have more than enough experience building a small Odoo module and want to skip ahead, feel free to reference our app (which you can find at the bottom with the sample code), and jump ahead to the next section.

Otherwise, let’s write our first piece of code that may need some tests.
A client has requested a new module that extends the Sales module. The client manufactures beer and they are legally required to track and display a list of ingredients for each product on their sales orders. After a short phone call with their team, we’ve talked through all of the use cases and decided to focus on the following requirements to start:

  • A sales manager can add a Recipe object to a product through the product configuration form.
  • A Recipe will track a list of ingredients with the product, the amount, and the amount unit to produce one serving.
  • When any user adds a product with a Recipe to a sales order, they can click the order line and see the ingredients.
  • A function is provided to calculate ingredients list adjusted by total number of servings.

2.1. Create a new module

To start, we need a module to work with.

  • 1. Create a folder called intro_to_testing.
  • 2. Add the following intro_to_testing/__manifest__.py file:
                    {    "name": "Introduction to Testing",    "summary": "Provides an example module for testing.",    "description": "Provides an example module for testing.",    "author": "Oocademy",    "website": "http://www.oocademy.com",    "category": "Testing",    "version": "15.0.0.1",    "depends": [        "base",        "product",        "sale",        "sale_management",    ],}                                    
  • 3. Make sure that your new intro_to_testing module is part of your addons_path in the odoo.conf file for your instance.
  • 4. Restart your Odoo instance and your module should be available under Apps now for install.

Module install view

2.2. Model the data

The data modeling for this module is pretty straight forward. We can get away from only a single custom model to track the ingredient lines of our recipe and relate them directly with the product template.

  • 1. Create your models directory with intro_to_testing/models/__init__.py:
                    from . import ingredientfrom . import product_templatefrom . import sale_order_line                                    
  • 2. Add the intro_to_testing/__init__.py:
                    from . import models                                    
  • 3. Add models/ingredient.py. Our Ingredient data model represents a single ingredient line within a recipe. Instead of creating a Recipe model, we can assume that each product has a single recipe and links the ingredient lines directly to the product for the sake of simplicity. Then our model simply has a reference to the ingredient product (product_id), the amount required for the recipe (amount), and the unit measurement for the amount (unit_id).
                    from odoo import fields, modelsclass Ingredient(models.Model):     _name = "recipe.ingredient"     _description = "Ingredient"     product_template_id = fields.Many2one(        "product.template",        string="Template",        required=True,     )     product_id = fields.Many2one(        "product.product",        string="Product",        required=True,     )     amount = fields.Float(        string="Amount",        required=True,     )     unit_id = fields.Many2one(        "uom.uom",        string="Unit",        required=True,        default=lambda self: self.env.ref("uom.product_uom_unit"),     )     def adjust_per_serving(self, servings: int):        self.ensure_one()        return self.amount * servings                                    
  • 4. Inherit product template in models/product_template.py. For the product template, we are adding the inverse side of the relationship for our Ingredient model.
                    from odoo import fields, modelsclass Product(models.Model):    _inherit = "product.template"    ingredient_ids = fields.One2many(        "recipe.ingredient",        "product_template_id",        string="Ingredients",    )                                    
  • 5. Inherit sale order line in models/sale_order_line.py. Finally, we have our sale order line inheritance. Users will be able to see the recipe from the sale order line, so we need to add a button action for them to click (show_recipe) and a way to know if the product on the order line even has a recipe (has_recipe calculation).
                    from odoo import api, fields, modelsclass SaleOrderLine(models.Model):    _inherit = "sale.order.line"    has_recipe = fields.Boolean(        string="Has Recipe",        compute="_compute_has_recipe",        store=True,    )    @api.depends("product_id.product_tmpl_id.ingredient_ids")    def _compute_has_recipe(self):        for line in self:            line.has_recipe = bool(line.product_id.product_tmpl_id.ingredient_ids)    def show_recipe(self):        self.ensure_one()        return {            "type": "ir.actions.act_window",            "view_mode": "form",            "res_model": "product.template",            "res_id": self.product_id.product_tmpl_id.id,            "view_id": self.env.ref("intro_to_testing.recipe_form").id,            "target": "new",        }                                    

2.3. Adding security

Since we are adding a brand new model with recipe.ingredient, we must add a security file. There will be no way for a user to access the data from the frontend without permissions defining who can access it.

We are going to create a security/ir.model.access.csv content with:

                    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlinkingredient_global,ingredient_global,model_recipe_ingredient,base.group_user,1,0,0,0ingredient_manager,ingredient_manager,model_recipe_ingredient,sales_team.group_sale_manager,1,1,1,1                                    

This allows users with the Sales > Administrator group to manage ingredients from the product view and for everyone else to be able to read the ingredients. Our intro_to_testing/__manifest__.py file needs to be updated:

                    {    ...    "data": [        "security/ir.model.access.csv",    ]}                                    

2.4. Adding views

Now that we have all of our data modeled and security ready to go, we need a way for users to actually access the data from the front end.

Since this module is simple, we can go ahead and have a single views.xml file for the sake of simplicity. We need view records for 4 items:

  • 1. Inherit the product template form to include a Recipe tab.
Ingredient model form view
  • 2. Create a new tree view for recipe.ingredient.
  • 3. Inherit the sale order form to include a button on the sale order lines to open recipe details.
Sale order model form view, adding ingredient button to order lines
  • 4. Create a recipe form view that gets opened from the sale order line.

Ingredient form view wizard pop-up, after clicking an ingredient order line

Include a `intro_to_testing/views.xml` file with the following:

                                                        

And again, our intro_to_testing/__manifest__.py file needs to be updated:

                    {    ...    "data": [        "views.xml",        ...    ]}                                    

3. Writing and running your first test

Tests in Odoo run directly on top of unittest like we discussed in earlier chapters. If you have any experience with unittest you’ll know that there are some requirements for tests to run. Odoo has a couple of additional requirements to be aware of before writing your first test:

  1. A test case class must be in a file that begins with test_.
  2. A test case class must inherit from odoo.tests.TransactionCase.
  3. A test case method must begin with test_.
  4. All tests files must be in a folder called tests inside of your module.
  5. Your tests folder must have an __init__.py which imports all your test files.
  6. Tests should only be run against a demo database.

3.1. Structuring a basic test class

Let’s start with creating the test class. Add a test class to intro_to_testing/tests/test_ingredients.py with a basic class and 2 dummy tests:

                    from odoo.tests import TransactionCaseclass IngredientTests(TransactionCase):    def test_should_pass(self):        self.assertTrue(True)    def test_should_fail(self):        self.assertTrue(False)                                    

And then import it via intro_to_testing/tests/__init__.py:

                    from .test_ingredients import *                                    

3.2. Running the test

To run our dummy tests, run your Odoo instance from the command line with some extra flags:

                     ./odoo-bin \ 
     -c path_to_your_odoo_conf_file \ 
     --xmlrpc-port=9999 \ 
     --longpolling-port=9998 \ 
     --test-enable \ 
     --stop-after-init \ 
     --logfile=/var/log/your_further_log_path_here \ 
     --log-level=info \ 
     --log-handler=:INFO \ 
     -d your_database_name_here \ 
     -i intro_to_testing                                     

Update this command with the path to your config file (typically somewhere like /etc/odoo.conf), the path to your logfile and the name of your database.

After running the command, you are going to see a lot of log messages from the application. There is a test runner which logs the details of each set of tests. This is a filtered example, but you should see similar messages:

Log output for the dummy tests

For our failed test, you get a stack trace to debug what’s going on.

3.3. Writing a unit test

Now that you know the process for creating and running a test, let’s actually write a test of substance. We can start with a unit test for calculating the proper serving amounts on ingredients.

We need to create data in the system to reference for testing. We can do that by either creating the record programmatically in Python within the test case setup method. This is not always recommended because your tests can fail if another module is installed in your system that inherits your data model.

A better way is to use demo data files.

Create another folder called data and add a file intro_to_testing/data/demo_ingredients.xml.

                                                        

And one last time, our intro_to_testing/__manifest__.py file needs to be updated to include our demo data:

                    {    ...    "demo": [        "data/demo_ingredients.xml",    ]}                                    

Once we have demo data, we can reference it from our tests via self.env.ref():

                    from odoo.tests import TransactionCaseclass IngredientTests(TransactionCase):    def test_single_serving_calculation(self):        ingredient = self.env.ref("intro_to_testing.demo_taco_ingredient_beef")        self.assertEqual(ingredient.adjust_per_serving(servings=1), ingredient.amount)        self.assertEqual(            ingredient.adjust_per_serving(servings=2), ingredient.amount * 2 ]]        )                                    

Each test method ultimately comes down to performing some function and checking that the result is expected. In this case, we are loading one of the ingredient lines that we created, running the adjust_per_serving() function with different inputs and asserting that the output is what we expect. For example, adjust_per_serving(1) should be the ingredient’s stored amount because it assumes 1 serving. adjust_per_serving(2) should be ingredient.amount * 2 since we are doubling the servings.

3.4. Creating a security test

Another one of our module requirements was that only certain user groups were allowed to manage our ingredients for specific products. Odoo has some tools built in tool with_user to help us create a security test by running a function as certain users:

                    from odoo.tests import TransactionCasefrom odoo.exceptions import AccessErrorclass IngredientTests(TransactionCase):    def test_sales_manager_can_manage_ingredients(self):        admin = self.env.ref("base.partner_admin")        ingredient = self.env.ref("intro_to_testing.demo_taco_ingredient_tortilla")        ingredient = ingredient.with_user(admin)        ingredient.amount = 3        self.assertEqual(ingredient.amount, 3)        ingredient.unlink()        self.assertFalse(ingredient.exists())    def test_non_sales_manager_cannot_manage_ingredients(self):        demo_user = self.env.ref("base.partner_demo")        ingredient = self.env.ref("intro_to_testing.demo_taco_ingredient_tortilla")        ingredient = ingredient.with_user(demo_user)        with self.assertRaises(AccessError):            ingredient.amount = 3        with self.assertRaises(AccessError):            ingredient.unlink()                                    

In our security test we are putting things in context of a specific user (either the demo user or admin user).

Looking at our test_sales_manager_can_manage_ingredients, we are doing exactly what the method says. We are testing that a user with the sales manager group has the ability to manage ingredients. So we load an ingredient record, put it in context of the proper user with with_user(admin), and then try to perform some functions on it like update or unlink.

Similarly with our test_non_sales_manager_cannot_manage_ingredients, we are making sure that an exception is raised when non sales manager users try to update or unlink.

Some other types of tests we could write here

This is by no means the complete extent of tests that we could write for this module. I hope it gives you a good idea about the general process for writing tests for your modules, but make sure to think about ways users or even other developers may break your code in the future. Then write tests to cover those cases.

Is a developer going to try to pass null values into your functions?

Is a user going to try to bypass security in some way to change ingredients?

Are there performance issues with the code? Maybe we should try to generate 1,000 ingredients and see if the system responds in a reasonable amount of time.

Thinking like a software tester could and has filled many books worth of information, but it all comes down to thinking about code from as many angles as possible. Be the ignorant user, the malicious user, the malicious hacker, etc. Write tests to mimic the behaviors of those users, and then defensively write your code to handle it!

4. A note on assertion types

You’ll see in my examples above that I used some assertions. Functions like self.assertTrue() or self.assertEquals().

These all come from the core unittest package and the core docs at https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual are extremely helpful to figure out what’s actually available to you.

In general, you can get away with a handful of assertion methods for almost all of your tests:

5. Wrapping up

I hope after going through this you have a strong enough grasp on the process of testing in Odoo to start testing your own code.

Future tutorials will cover more specific topics such as testing the frontend of the application, handling performance testing, more helper functions built into the Odoo core packages, and creating test fixtures or mocks.

Make sure to keep writing and running those tests and you’ll quickly see the benefit of having a safety net of use cases automatically run every time you type a simple command into your terminal!

Good luck.