Does anyone know how can you write mock tests for Odoo objects?
I have these classes and methods:
my_module:
from odoo import models
class MyModel(models.Model):
_name = 'my.model'
def action_copy(self):
IrTranslation = self.env['ir.translation']
for rec in self:
if rec.translate:
IrTranslation.force_translation(rec)
my_module_2:
from odoo import models
class IrTranslation(models.Model):
_inherit = 'ir.translation'
def force_translation(self, rec):
# do stuff
When I call it, I want to test if IrTranslation.force_translation was called in action_copy method and how many times.
But this method is not imported directly, it is referenced through env.
If let say force_translation would be imported like:
from my_module_2.IrTranslation import force_translation
def action_copy(self):
# do stuff.
force_translation()
Then I could try doing something like this:
from unittest import mock
from my_module import action_copy
def test_some_1(self):
with mock.patch('my_module.my_module_2.IrTranslation') as mocked_translation:
action_copy()
mocked_translation.force_translation.assert_called_once()
But because modules in Odoo are not imported directly (like you do it in plain Python), I don't understand how to specify methods in Odoo environment to be mocked.
P.S. I also did not see any mocked tests in standard Odoo, except for base classes that do not inherit Model class -> which then you need to use its _inherit attribute instead of importing class and passing it to be inherited on another class.
Testing in Odoo does not use the concept of mocking. Instead, tests are derived from standard base classes. The standard class TransactionalTest opens a transaction and never commits it, but rolls it back to undo any changes.
This is obviously not the same as regular mocking in that you can't replace other methods or classes to return fixed/expected values and/or avoid other side effects apart from persisting changes in the database, like sending emails or calling a remote web service.
It can be done. I do it all the time since Odoo 8.0 (until 15.0 now). The key is to know where to patch. Odoo adds odoo.addons to your module's package when its imported so in your case, you may do the following:
from odoo import tests
from mock import patch
from odoo.addons.my_module_2.models.ir_translations import IrTranslation
class TestMyModule2(tests.TransactionCase):
def some_test_1(self):
my_model = self.env['my.model'].create({})
with patch.object(IrTranslation, 'force_translation') as mocked_translation:
my_model.action_copy()
mocked_translation.assert_called_once()
Or using just patch, then no need to import:
with patch('odoo.addons.my_module_2.models.ir_translations.IrTranslation.force_translation') as mocked_translation:
my_model.action_copy()
This patches your specific method in your specific class. This way you can also target the method of a super class.
If you need to patch a method and you don't care where it is or where it's overriden, just patch using Python's type() (then no need to import class):
with patch.object(type(self.env['ir.translation']), 'force_translation') as mocked_translation:
my_model.action_copy()
Some additional notes to save you some headaches:
If you use pyCharm, don't mock socket objects. It messes with
pyCharm's mechanismes. Better to put your calls to socket into a one line
method and mock that method instead.
datetime.datetime.now() cannot be mocked, as all builtin types, but fields.Datetime.now() can.
Related
I am unittesting my new librabry, which is basically database interface. Our apps use it to access our database. that means, I want to test all methods, but I do not want DB commands to be called for real. I only check if they are called with correct arguemnts.
For that purpose, I am mocking the database library. this is the actual code that DOES work:
import pytest
from unittests.conftest import PyTestConfig as Tconf
from my_lib.influx_interface import InfluxInterface
class TestInfluxInterface:
def test_connect(self, mocker):
"""
Create InfluxConnector object and call connect()
check if __init__() arguments are passed / used correctly
"""
influx_client = mocker.patch('my_lib.influx_interface.InfluxDBClient', autospec=True)
test_connector = InfluxInterface(Tconf.test_id)
# Call connect with no input (influx_client - should be called with no arguemnts
test_connector.connect()
influx_client.assert_called_once()
influx_client.reset_mock()
# Call connect with custom correct input (influx_client - should be called with custom values
test_connector.connect(Tconf.custom_conf)
influx_client.assert_called_once_with(url=Tconf.custom_conf["url"], token=Tconf.custom_conf["token"],
org=Tconf.custom_conf["org"], timeout=Tconf.custom_conf["timeout"],
debug=Tconf.custom_conf["debug"])
influx_client.reset_mock()
# Call connect with incorrect input (influx_client - should be called with default values
test_connector.connect(Tconf.default_conf)
influx_client.assert_called_once_with(url=Tconf.default_conf["url"], token=Tconf.default_conf["token"],
org=Tconf.default_conf["org"], timeout=Tconf.default_conf["timeout"],
debug=Tconf.default_conf["debug"])
Now, what I do next, is to add more methods into TestInfluxInterface class, which will be testing rest of the code. One test method for each method in my library. Thats how I usually do it.
The problem is, that there is a part of the code:
influx_client = mocker.patch('my_lib.influx_interface.InfluxDBClient', autospec=True)
test_connector = InfluxInterface(Tconf.test_id)
That will be same for every method. Thus I will be copy-pasting it over and over. As you can already see, thats not good solution.
In unittest, I would do this:
import unittest
import unittest.mock as mock
from unittests.conftest import PyTestConfig as Tconf
from my_lib.influx_interface import InfluxInterface
#mock.patch('my_lib.influx_interface.InfluxDBClient', autospec=True)
class TestInfluxInterface:
def setUp(self):
self.test_connector = InfluxInterface(Tconf.test_id)
def test_connect(self, influx_client):
"""
Create InfluxConnector object and call connect()
check if __init__() arguments are passed / used correctly
"""
# Call connect with no input (influx_client - should be called with no arguemnts
self.test_connector.connect()
influx_client.assert_called_once()
Than in each method, I would use self.test_connector to call whatever method I want to test, and check if it called correct influx_client method, with correct parameters.
However, we are moving from unittest to pytest. And while I am wrapping my head around pytest docs, reading stuff here and there, fixtures, mockers, etc..I cannot find out how to do this correctly.
I want to write unit tests for the main.py class. The file structure of my project is like this,
my unit tests are included in test/source/code_files folder. I want to mock some methods in the main class too. (which uses variables in source/config/config.py) I'm using the patch for this.
ex:
import main
#patch('main.config.retry_times')
def test_method(self, mock_retry_times):
mock_retry_times().return_value = 'retry_times_mock_val'
#calling the main class method
in the main class method, retry_times is defined like this,
from source.config import config
def method():
var1 = config.retry_times['url']
# Do other stuff
This gave me an error as,
I tried with Magic Mock object as well. But it didn't work as per this solution. My imports also work fine.
But I figure out one thing.
when I check mock IDs in both the testable class and the test class they were different like this.
It seems like an issue in these ids. I think they must be the same in both classes. Does anyone help me to sortout this issue.
Firstly, thank you for taking the time to read and input. It is greatly appreciated.
Question: What kind of approach can we take to keep the same public API of a class currently using multiple mixins but refactor it internally to be composed of objects that do the same work as the mixin. Autocomplete is a must (so runtime dynamics are kind of out such as hacking things on via __getattr__ or similar - I know this depends on the runtime environment i.e ipython vs pycharm etc, for the sake of this question, assume pycharm which cannot leverage __dir__ I think fully.
Accompanying Information:
I am writing a little assertion library in python and I have a core class which is instantiated with a value and subsequently inherits various assertion capabilities against that value via a growing number of mixin classes:
class Asserto(StringMixin, RegexMixin):
def __init__(self, value: typing.Any, type_of: str = AssertTypes.HARD, description: typing.Optional[str] = None):
self.value = value
self.type_of = type_of
self.description = description
These mixin classes offer various assertion methods for particular types, here is a quick example of one:
from __future__ import annotations
class StringMixin:
def ends_with(self, suffix: str) -> StringMixin:
if not self.value.endswith(suffix):
self.error(f"{self.value} did not end with {suffix}")
def starts_with(self, prefix: str) -> StringMixin:
if not self.value.startswith(prefix):
self.error(f"{self.value} did not end with {prefix}")
I would like to refactor the Asserto class to compose itself of various implementations of some sort of Assertable interface rather than clobber together a god class here with Mixins, I'm likely to have 10+ Mixins by the time I am finished.
Is there a way to achieve the same public facing API as this mixins setup so that client code has access to everything through the Asserto(value).check_something(...) but using composition internally?
I could define every single method in the Asserto class that just delegate to the appropriate concrete obj internally but then I am just making a massive god class anyway and the composition feels like a pointless endeavour in that instance?
for example in client code, I'd like all the current mixins methods to be available on an Asserto instance with autocomplete.
def test_something():
Asserto("foo").ends_with("oo")
Thank you for your time. Perhaps using the mixin approach is the correct way here, but it feels kind of clunky.
I'm using FastAPI with Pydantic and I'm trying to achieve that my API accepts cammel case parameters, for this, I'm using the following
from pydantic import BaseModel
from humps import camelize
class CamelModel(BaseModel):
class Config:
alias_generator = camelize
allow_population_by_field_name = True
class MyClass(CamelModel):
my_field1: int
my_field2: int
my_field3: int
So far it works great, but MyClass is a base class for others classes, for example as
class MyNewClass(MyClass):
my_field4: float
How can I get the MyNewClass to also use the camel case base class? I've tried something like
from typing import Union
class MyNewClass(Union[MyClass, CamelModel]):
my_field4: float
But I'm getting this error
TypeError: Cannot subclass <class 'typing._SpecialForm'>
Is there any way to accomplish this?
Thanks!
What you are trying to achieve is called multiple inheritance. Since you are inheriting from a class which inherited from the CamelModel, there's no need to inherit it again
The appropriate code should be
class MyNewClass(MyClass):
It's the python syntax for multiple inheritance. See an extensive example here https://www.python-course.eu/python3_multiple_inheritance_example.php#An-Example-of-Multiple-Inheritance
The code you are using (class MyNewClass(Union[MyClass, CamelModel]):) is used for declaring data types, which is kinda of correct, but not in the right place. Typing is almost only (as far as I've seen) used for parameters of functions.
NOTE
I did not test the piece of code above, but I'm pretty sure it works. Let me know if there are any problems
I saw an example of peewee where pysqlcipher was used as the connector for managing a database file rather than the sqlite module. That's great and there's even an async version of peewee but I don't need (Or want) to use peewee's object model. In peewee, the connector is initialized like this:
from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase
db = SqlCipherDatabase(None)
class Entry(Model):
class Meta:
database = db
I want to do something similar with aiosqlite and pysqlcipher3 instead of using peewee. Maybe it can work by overriding aiosqlite.Connection but I've never done something like that before. How can I use pysqlcipher3 with aiosqlite?
aiosqlite uses the standard library sqlite3 module -- and that appears to be hardcoded here:
https://github.com/omnilib/aiosqlite/blob/master/aiosqlite/core.py
In addition they've sprinkled all kinds of sqlite3-specific type annotations all over the place, so I'm not sure whether you can even monkey-patch it without causing issues.