Skip to content

Commit

Permalink
Rewrite tests using new API and vcr.py
Browse files Browse the repository at this point in the history
* Use vcr.py to record the exchanges instead of a self-made recorder
* Use new base Components test case classes
* Separate export tests in 2 phases: trigger of the export which check
  that the jobs are delayed, and test the job itself in a second test
  • Loading branch information
guewen committed Jul 10, 2017
1 parent 35ca26b commit 7d0c67f
Show file tree
Hide file tree
Showing 63 changed files with 21,647 additions and 29,253 deletions.
43 changes: 0 additions & 43 deletions connector_magento/components/backend_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,6 @@
MAGENTO_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'


recorder = {}


# TODO: use vcr.py?
def call_to_key(method, arguments):
""" Used to 'freeze' the method and arguments of a call to Magento
so they can be hashable; they will be stored in a dict.
Used in both the recorder and the tests.
"""
def freeze(arg):
if isinstance(arg, dict):
items = dict((key, freeze(value)) for key, value
in arg.iteritems())
return frozenset(items.iteritems())
elif isinstance(arg, list):
return tuple([freeze(item) for item in arg])
else:
return arg

new_args = []
for arg in arguments:
new_args.append(freeze(arg))
return (method, tuple(new_args))


def record(method, arguments, result):
""" Utility function which can be used to record test data
during synchronisations. Call it from MagentoCRUDAdapter._call
Then ``output_recorder`` can be used to write the data recorded
to a file.
"""
recorder[call_to_key(method, arguments)] = result


def output_recorder(filename):
import pprint
with open(filename, 'w') as f:
pprint.pprint(recorder, f)
_logger.debug('recorder written to file %s', filename)


class MagentoLocation(object):

def __init__(self, location, username, password,
Expand Down
2 changes: 1 addition & 1 deletion connector_magento/components/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def run(self, external_id, force=False):
lock_name = 'import({}, {}, {}, {})'.format(
self.backend_record._name,
self.backend_record.id,
self.model._name,
self.work.model_name,
external_id,
)

Expand Down
62 changes: 16 additions & 46 deletions connector_magento/doc/project/contribute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,48 +202,14 @@ Every new feature in the connector should have tests. We use exclusively the

The tests are located in ``connector_magento/tests``.

The tests run without any connection to Magento. They mock the API. In order
to test the connector with representative data, we record real
responses/requests, then use them in the tests. The reference data we use are
those of the Magento demo, which are automatically installed when you install
Magento using theses instructions: `Magento on Docker`_.
The tests run without any connection to Magento. They use `vcr.py
<https://vcrpy.readthedocs.io/en/latest/>`_ in order to record real requests
made towards the Magento API. The first time a test is run, vrcpy runs the
request on a real Magento, the next times the test is run, it uses the
registered data.
The reference data we use are those of the Magento demo data.

Thus, in the ``tests`` folder, you will find files with only data, and the
others with the tests.

In order to record data, you can proceed as follows:

In ``connector_magento/unit/backend_adapter.py`` at lines 130,130:

.. code-block:: python
:emphasize-lines: 7,8
def _call(self, method, arguments):
try:
with magentolib.API(self.magento.location,
self.magento.username,
self.magento.password) as api:
result = api.call(method, arguments)
# Uncomment to record requests/responses in ``recorder``
# record(method, arguments, result)
_logger.debug("api.call(%s, %s) returned %s",
method, arguments, result)
return result
Uncomment the line doing a call to :py:func:`~openerp.addons.connector_magento.unit.backend_adapter.record()`.
Then, as soon as you will start the server, all the requests and responses
will be stored in global dict. Once you have recorded some exchanges, you can
output them using a tool such as `ERPpeek`_ and by calling the method
:py:class:`~openerp.addons.connector_magento.magento_model.magento_backend.output_recorder`:

.. code-block:: python
client.MagentoBackend.get(1).output_recorder([])
A path is returned with the location of the file.

When you want to use a set of test data in a test, just use
:py:func:`~openerp.addons.connector_magento.tests.common.mock_api()`:

.. code-block:: python
Expand All @@ -253,15 +219,19 @@ When you want to use a set of test data in a test, just use
<...>
def test_new(self):
<...>
with mock_api(new_set_of_data):
with recorder.use_cassette(
'test_export_xxx') as cassette:
# do what the test needs, such as, for instance:
import_batch(self.session, 'magento.website', backend_id)
binding.export_record()
# all http calls are recorded in 'cassette'
# we can now check many things in the cassette itself
self.assertEqual(1, len(cassette.requests))
See how to `Run the tests`_

Useful links:

* unittest documentation: http://docs.python.org/dev/library/unittest.html
* Odoo's documentation on tests: https://doc.openerp.com/trunk/server/05_test_framework/

.. _`ERPpeek`: https://erppeek.readthedocs.org/en/latest/
* unittest documentation: https://docs.python.org/2/library/unittest.html
* Odoo's documentation on tests: https://www.odoo.com/documentation/10.0/reference/testing.html
* vcr.py documentation: https://vcrpy.readthedocs.io/en/latest/
* pytest odoo plugin: https://pypi.python.org/pypi/pytest-odoo
10 changes: 5 additions & 5 deletions connector_magento/models/account_invoice/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class MagentoAccountInvoice(models.Model):
@job(default_channel='root.magento')
@related_action(action='related_action_unwrap_binding')
@api.multi
def export_invoice(self):
def export_record(self):
""" Export a validated or paid invoice. """
self.ensure_one()
with self.backend_id.work_on(self._name) as work:
Expand Down Expand Up @@ -105,7 +105,7 @@ class MagentoBindingInvoiceListener(Component):
_apply_on = ['magento.account.invoice']

def on_record_create(self, record, fields=None):
record.with_delay().export_invoice()
record.with_delay().export_record()


class MagentoInvoiceListener(Component):
Expand Down Expand Up @@ -139,9 +139,9 @@ def invoice_create_bindings(self, invoice):
# Check if invoice state matches configuration setting
# for when to export an invoice
magento_store = magento_sale.store_id
payment_method = sale.payment_mode_id
if payment_method and payment_method.create_invoice_on:
create_invoice = payment_method.create_invoice_on
payment_mode = sale.payment_mode_id
if payment_mode and payment_mode.create_invoice_on:
create_invoice = payment_mode.create_invoice_on
else:
create_invoice = magento_store.create_invoice_on

Expand Down
16 changes: 0 additions & 16 deletions connector_magento/models/magento_backend/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,22 +338,6 @@ def _scheduler_import_product_product(self, domain=None):
def _scheduler_update_product_stock_qty(self, domain=None):
self._magento_backend('update_product_stock_qty', domain=domain)

@api.multi
def output_recorder(self):
""" Utility method to output a file containing all the recorded
requests / responses with Magento. Used to generate test data.
Should be called with ``erppeek`` for instance.
"""
from .unit.backend_adapter import output_recorder
import os
import tempfile
fmt = '%Y-%m-%d-%H-%M-%S'
timestamp = datetime.now().strftime(fmt)
filename = 'output_%s_%s' % (self.env.cr.dbname, timestamp)
path = os.path.join(tempfile.gettempdir(), filename)
output_recorder(path)
return path


class MagentoConfigSpecializer(models.AbstractModel):
_name = 'magento.config.specializer'
Expand Down
6 changes: 3 additions & 3 deletions connector_magento/models/product/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from odoo import models, fields, api
from odoo.addons.connector.exception import IDMissingInBackend
from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if
from odoo.addons.queue_job.job import job, related_action
from ...components.backend_adapter import MAGENTO_DATETIME_FORMAT

Expand Down Expand Up @@ -240,7 +241,7 @@ def update_inventory(self, id, data):

class MagentoBindingProductListener(Component):
_name = 'magento.binding.product.product.listener'
_inherit = 'base.event.listener'
_inherit = 'base.connector.listener'
_apply_on = ['magento.product.product']

# fields which should not trigger an export of the products
Expand All @@ -250,9 +251,8 @@ class MagentoBindingProductListener(Component):
'magento_qty',
)

@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_write(self, record, fields=None):
if self.env.context.get('connector_no_export'):
return
if record.no_stock_sync:
return
inventory_fields = list(
Expand Down
20 changes: 11 additions & 9 deletions connector_magento/models/sale_order/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ def write(self, vals):
self._magento_cancel()
return super(SaleOrder, self).write(vals)

def _magento_link_binding_of_copy(self, new_id):
def _magento_link_binding_of_copy(self, new):
# link binding of the canceled order to the new order, so the
# operations done on the new order will be sync'ed with Magento
if self.state != 'cancel':
return
binding_model = self.env['magento.sale.order']
bindings = binding_model.search([('odoo_id', '=', self.id)])
bindings.write({'odoo_id': new_id})
bindings.write({'odoo_id': new.id})

for binding in bindings:
# the sales' status on Magento is likely 'canceled'
Expand All @@ -139,11 +141,11 @@ def _magento_link_binding_of_copy(self, new_id):
).export_state_change()

@api.multi
def copy_quotation(self):
def copy(self, default=None):
self_copy = self.with_context(__copy_from_quotation=True)
result = super(SaleOrder, self_copy).copy_quotation()
self._magento_link_binding_of_copy(result['res_id'])
return result
new = super(SaleOrder, self_copy).copy(default=default)
self_copy._magento_link_binding_of_copy(new)
return new


class MagentoSaleOrderLine(models.Model):
Expand Down Expand Up @@ -220,7 +222,7 @@ def create(self, vals):

@api.multi
def copy_data(self, default=None):
data = super(SaleOrderLine, self).copy_data(default=default)
data = super(SaleOrderLine, self).copy_data(default=default)[0]
if self.env.context.get('__copy_from_quotation'):
# copy_data is called by `copy` of the sale.order which
# builds a dict for the full new sale order, so we lose the
Expand All @@ -229,8 +231,8 @@ def copy_data(self, default=None):
# to `create`, from there, we'll be able to update the
# Magento bindings, modifying the relation from the old to
# the new line.
data['__copy_from_line_id'] = id
return data
data['__copy_from_line_id'] = self.id
return [data]


class SaleOrderAdapter(Component):
Expand Down
2 changes: 1 addition & 1 deletion connector_magento/models/sale_order/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def project_id(self, record):
def fiscal_position(self, record):
fiscal_position = self.options.storeview.fiscal_position_id
if fiscal_position:
return {'fiscal_position': fiscal_position.id}
return {'fiscal_position_id': fiscal_position.id}

# partner_id, partner_invoice_id, partner_shipping_id
# are done in the importer
Expand Down
2 changes: 1 addition & 1 deletion connector_magento/models/stock_picking/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def on_tracking_number_added(self, record):
for binding in record.magento_bind_ids:
# Set the priority to 20 to have more chance that it would be
# executed after the picking creation
binding.with_delay(priority=20).export_tracking()
binding.with_delay(priority=20).export_tracking_number()

def on_picking_out_done(self, record, picking_method):
"""
Expand Down
30 changes: 7 additions & 23 deletions connector_magento/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from . import test_synchronization
from . import test_address_book
from . import test_concurrent_sync
from . import test_export_invoice
from . import test_export_picking
from . import test_export_product_stock
from . import test_import_metadata
from . import test_import_partner
from . import test_import_partner_category
from . import test_import_product
from . import test_import_product_category
from . import test_import_product_image
from . import test_related_action
from . import test_sale_order
from . import test_export_picking
from . import test_update_product_stock
Loading

0 comments on commit 7d0c67f

Please sign in to comment.