Testing in Tryton



Testing in Tryton

1 2


testing-in-tryton

Testing in Tryton slidesd for Jornadas Tryton 2016

On Github pokoli / testing-in-tryton

Testing in Tryton

Available on: http://pokoli.github.io/testing-in-tryton

Who am I?

Sergi Almacellas Abellana

  • (Python ♥) Programmer
  • Open source enthusiast
  • Tryton commiter since Nov 2013

Why testing?

Quality control Find problems early Prevent regressions Code documentation Faster Development Reduce change fear

Test Driven Development

Write Test Case Ensure Test Fails Implement Functionality Ensure Test Pass Refractor Ensure Test Pass

Types of testing

  • Unit Testing
  • Scenarios

When to use each?

  • Computations → Unit Testing
  • Workflows & User Interaction → Scenarios

Basic Unit Testing

from trytond.tests.test_tryton import ModuleTestCase


class AccountTestCase(ModuleTestCase):
    'Test Account module'
    module = 'account'


def suite():
    suite = trytond.tests.test_tryton.suite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
        AccountTestCase))
    return suite
                    

Basic Unit Testing

13 lines of code

Which tests:

  • Module instalation
  • Views validity
  • Fields dependency
  • Menuitem permisions
  • Model access
  • default_* methods
  • order_* methods
  • Workflow transitions
  • Model's rec_name

Real Unitests

from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool

from trytond.modules.company.tests import create_company, set_company


class ProductPriceListTestCase(ModuleTestCase):
    'Test ProductPriceList module'
    module = 'product_price_list'

    @with_transaction()
    def test_price_list(self):
        'Test price_list'
        pool = Pool()
        Template = pool.get('product.template')
        Product = pool.get('product.product')
        Party = pool.get('party.party')
        Uom = pool.get('product.uom')
        PriceList = pool.get('product.price_list')

        company = create_company()
        with set_company(company):
            party = Party(name='Customer')
            party.save()

            kilogram, = Uom.search([
                    ('name', '=', 'Kilogram'),
                    ])
            gram, = Uom.search([
                    ('name', '=', 'Gram'),
                    ])

            template = Template(
                name='Test Lot Sequence',
                list_price=Decimal(10),
                cost_price=Decimal(5),
                default_uom=kilogram,
                )
            template.save()
            product = Product(template=template)
            product.save()
            variant = Product(template=template)
            variant.save()

            price_list, = PriceList.create([{
                        'name': 'Default Price List',
                        'lines': [('create', [{
                                        'quantity': 10.0,
                                        'product': variant.id,
                                        'formula': 'unit_price * 0.8',
                                        }, {
                                        'quantity': 10.0,
                                        'formula': 'unit_price * 0.9',
                                        }, {
                                        'product': variant.id,
                                        'formula': 'unit_price * 1.1',
                                        }, {
                                        'formula': 'unit_price',
                                        }])],
                        }])
            tests = [
                (product, 1.0, kilogram, Decimal(10.0)),
                (product, 1000.0, gram, Decimal(10.0)),
                (variant, 1.0, kilogram, Decimal(11.0)),
                (product, 10.0, kilogram, Decimal(9.0)),
                (product, 10000.0, gram, Decimal(9.0)),
                (variant, 10.0, kilogram, Decimal(8.0)),
                (variant, 10000.0, gram, Decimal(8.0)),
                ]
            for product, quantity, unit, result in tests:
                self.assertEqual(
                    price_list.compute(party, product, product.list_price,
                        quantity, unit),
                    result)
                    

Real Unitests

  • One transaction per unittest
  • Full database setup
  • With helpers (i.e.: create company)
  • Standard unittest (self.assertEqual)
  • Run in server side

Scenarios

from trytond.tests.test_tryton import (doctest_setup, doctest_teardown,
    doctest_checker)


def suite():
    suite = trytond.tests.test_tryton.suite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(SaleTestCase))
    suite.addTests(doctest.DocFileSuite('scenario_sale.rst',
            setUp=doctest_setup, tearDown=doctest_teardown, encoding='utf-8',
            optionflags=doctest.REPORT_ONLY_FIRST_FAILURE,
            checker=doctest_checker))
    return suite
                    
On new database per scenario Standard doctests (rst files) Build using proteus

Scenarios: Why proteus?

Mimics tryton client

Allows:

  • Transparently calling on_change
  • Clicking buttons
  • Execute Wizards
  • Execute reports
  • Check User Permisions

Scenarios Examples

Helpers available


Create company::

    >>> _ = create_company()
    >>> company = get_company()

Create chart of accounts::

    >>> _ = create_chart(company)
    >>> accounts = get_accounts(company)
    >>> receivable = accounts['receivable']
    >>> revenue = accounts['revenue']
    >>> expense = accounts['expense']
    >>> account_tax = accounts['tax']
    >>> account_cash = accounts['cash']
                    

Scenarios Examples

Calling on_change

    >>> Invoice = Model.get('account.invoice')
    >>> invoice = Invoice()
    >>> invoice.party = party
    >>> invoice.payment_term = payment_term
    >>> invoice.lines.new()
    >>> line.product = product
    >>> line.quantity = 5
    >>> line.unit_price = Decimal('40')
    >>> invoice.untaxed_amount
    Decimal('200.00')
    >>> invoice.tax_amount
    Decimal('20.00')
    >>> invoice.total_amount
    Decimal('220.00')
    >>> invoice.save()
                    

Scenarios Examples

Clicking Buttons

    >>> sale.click('quote')
    >>> sale.click('confirm')
    >>> sale.click('process')
    >>> sale.state
    u'processing'
    >>> invoice, = sale.invoices
    >>> invoice.click('post')
                    

Scenarios Examples

Execute Wizard

    >>> from proteus import Wizard
    >>> Wizard('ir.module.install_upgrade').execute('upgrade')
                    

With forms and models:

    >>> from proteus import Wizard
    >>> credit = Wizard('account.invoice.credit', [invoice])
    >>> credit.form.with_refund = True
    >>> credit.execute('credit')
                    

Scenarios Examples

Print Reports

    >>> from proteus import config, Model, Wizard, Report
    >>> GeneralLedgerAccount = Model.get('account.general_ledger.account')
    >>> gl_accounts = GeneralLedgerAccount.find([])
    >>> _ = [(l.balance, l.party_required) for gl in gl_accounts
    ...     for l in gl.lines]

    >>> general_ledger = Report('account.general_ledger', context={
    ...     'company': company.id,
    ...     'fiscalyear': fiscalyear.id,
    ...     })
    >>> _ = general_ledger.execute(gl_accounts)

    >>> sale_report = Report('sale.sale')
    >>> ext, _, _, name = sale_report.execute([sale], {})
    >>> ext
    u'odt'
    >>> name
    u'Sale'
                    

Scenarios Examples

User permisions

    >>> sale_user = User()
    >>> stock_user = User()

Create sale::

    >>> config.user = sale_user.id
    >>> sale = Sale()
    ...
    >>> sale.click('process')

Process shipment::

    >>> shipment, = sale.shipments
    >>> config.user = stock_user.id
    >>> shipment.click('assign_try')
    True
    >>> shipment.click('pack')
    >>> shipment.click('done')
                    

Tox

[tox]
envlist = {py27,py33,py34,py35}-{sqlite,postgresql,mysql},pypy-{sqlite,postgresql}

[testenv]
commands = {envpython} setup.py test
deps =
    {py27,py33,py34,py35}-postgresql: psycopg2 >= 2.5
    pypy-postgresql: psycopg2cffi >= 2.5
    mysql: MySQL-python
setenv =
    sqlite: TRYTOND_DATABASE_URI={env:SQLITE_URI:sqlite://}
    postgresql: TRYTOND_DATABASE_URI={env:POSTGRESQL_URI:postgresql://}
    mysql: TRYTOND_DATABASE_URI={env:MYSQL_URI:mysql://}
    sqlite: DB_NAME={env:SQLITE_NAME::memory:}
    postgresql: DB_NAME={env:POSTGRESQL_NAME:test}
    mysql: DB_NAME={env:MYSQL_NAME:test}
install_command = pip install --pre --find-links https://trydevpi.tryton.org/ {opts} {packages}
                    

Tox

On single command to run on:

  • Multiple databases
  • Multiple python versions
  • Each Database on Each python version

Currently testing on:

  • PostgreSQL/SQLite
  • py27,py33,py34,py35

Drone

https://drone.tryon.org/

Drone

.drone.yml file with:

image: python:all
env:
  - POSTGRESQL_URI=postgresql://postgres@127.0.0.1:5432/
  - MYSQL_URI=mysql://root@127.0.0.1:3306/
script:
  - pip install tox
  - tox -e "{py27,py33,py34,py35}-{sqlite,postgresql}" --skip-missing-interpreters
services:
  - postgres
                    

Executed on a docker container

Build Status

Based on latest drone result

https://tests.tryton.org/

Migrating modules to new versions

Migrations ♥ Tests

My steps to migrate to new version:

Write tests (if none) Migrate tests (if required) Update dependencies Run tests Fix errors Repeat untill all green

Other improvements:

Test Code Quality (pep8, pyflakes)

Coverage

Thank you!

The presentation code is avaiable on

http://github.com/pokoli/testing-in-tryton

Testing in Tryton Available on: http://pokoli.github.io/testing-in-tryton