Our app is pretty good at allowing the purchaser to enter the price in Schrute Bucks, but there is one problem. It looks like the user is able to enter any text in the field. We want the user to only be able to enter a number.
Ideally, we'll want to override the create method to add a check to ensure that the value in the field is a number. However, we see this becoming a large project so we want to ensure that any features that we add in the future will not break our check that we have yet to write. We're going to adopt a methodology called test driven development. Test Driven Development means that we want to write the test first for the expected behavior and then we'll write the code to pass this test. The test will fail the first time that you run it because there's no code in place to change this behavior.
Similarly when we defined a model, we'll make a tests
subfolder. This is not just a convention but Odoo is programmed to check a subfolder named tests
to find any tests to run for the module.
You will need an __init__.py
file which will import any test files so that Odoo knows what to run.
from . import test_schrute_bucks
Any test file will need to begin with test_
. For test_schrute_bucks.py
, we're going to use the most common test case that Odoo provides for us to help define our class, common.TransactionCase
, which we'll import from the Odoo base tests. Now, we'll create a simple method called test_is_number
which uses the with
context manager keyword to make sure that when we attempt to create a record with test in the schrute_bucks
field, a ValidationError
will be raised. Finally, notice when creating the record, we must also specify a valid partner_id
in the dictionary. This is because when we create a record for purchase.order
on the web, a Vendor
field is required.
Notice that Order Deadline
doesn't need to be filled out because this is automatically generated when the record is created.
Further reading on Odoo python unit testing can be found here
from odoo.tests import common
from odoo.exceptions import ValidationError
class TestSchruteBucks(common.TransactionCase):
def test_is_number(self):
with self.assertRaises(ValidationError):
self.env['purchase.order'].create(
{
'partner_id': 2,
'schrute_bucks': 'test_val'
}
)
Running a test can be confusing when you first write a test in Odoo. From the documentation you want to both upgrade the module with the test that you want to run and include the --test-enable
flag on the command line. Doing this will start Odoo as normal but search the module that you've specified for the tests
subfolder and run any tests before starting the server. I include any other flags that I want to specify in a configuration file so that I can fun the following command: python ./odoo-bin -c run.conf -u purchase_schrute_bucks --test-enable
. For convenience and because I already use the virtual environment pipenv
from a previous post, I usually include this as a separate command: test = python ./odoo-bin -c run.conf -u purchase_schrute_bucks --test-enable
so that I can just enter pipenv run test
What can also be confusing is that any output is included with your other logs so you may need to search your logs to find the result. After a little digging, I've found a failed test that my error was never raised.
2022-07-17 20:31:12,321 24208 ERROR odoo-test4 odoo.addons.purchase_schrute_bucks.tests.test_schrute_bucks: FAIL: TestSchruteBucks.test_is_number
Traceback (most recent call last):
File "Projects/odoo15/odoo/customaddons/purchase_schrute_bucks/tests/test_schrute_bucks.py", line 11, in test_is_number
'schrute_bucks': 'test_val'
File "/usr/lib/python3.7/contextlib.py", line 119, in __exit__
next(self.gen)
File "Projects/odoo15/odoo/odoo/tests/common.py", line 436, in _assertRaises
yield cm
File "/usr/lib/python3.7/contextlib.py", line 524, in __exit__
raise exc_details[1]
File "/usr/lib/python3.7/contextlib.py", line 509, in __exit__
if cb(*exc_details):
File "/usr/lib/python3.7/contextlib.py", line 377, in _exit_wrapper
return cm_exit(cm, exc_type, exc, tb)
AssertionError: ValidationError not raised
This is what we want. We want the test to fail so that I can write some code to successfully raise the error and pass the test.
We will add to purchase.py
from the last post and override the create
method to attempt to convert our value to a number using the int
method. If we can convert this without an error, we will proceed with the record creation using a call to super()
. If we can't convert this value, we'll raise a validation error.
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
schrute_bucks = fields.Char(string='Price in Schrute Bucks', required=True)
@api.model
def create(self, vals):
if 'schrute_bucks' in vals:
try:
int(vals.get('schrute_bucks'))
except ValueError as ver:
raise ValidationError(f'Unable to convert {vals.get("schrute_bucks")}. Error: {ver}')
return super().create(vals)
Here's what this looks like on the web when the error is generated . Odoo won't allow you to save the record until you enter a valid number.
We should have the code now that's able to properly pass the test. One thing that I've found challenging and don't know a solution for is getting test results. You'll be able to see an Info log that was generated like
2022-07-17 21:31:04,777 4029 INFO odoo-test4 odoo.addons.purchase_schrute_bucks.tests.test_schrute_bucks: Starting TestSchruteBucks.test_is_number ...
However, I like to do a little customization and include a print statement after that assert because an assertion error will be raised anyway if the assertion fails. So here, I'll just add a simple print statement. Depending on the system that you're using, you can also go further and customize the colors to make this print statement pop out more. You can also modify the test to have this written to a test-results file
from odoo.tests import common
from odoo.exceptions import ValidationError
class TestSchruteBucks(common.TransactionCase):
def test_is_number(self):
with self.assertRaises(ValidationError):
self.env['purchase.order'].create(
{
'partner_id': 2,
'schrute_bucks': '6'
}
)
print('Schrute Bucks has passed')