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?

  1. Quality control
  2. Find problems early
  3. Prevent regressions
  4. Code documentation
  5. Faster Development
  6. Reduce change fear

Test Driven Development

  1. Write Test Case
  2. Ensure Test Fails
  3. Implement Functionality
  4. Ensure Test Pass
  5. Refractor
  6. 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:

    1. Write tests (if none)
    2. Migrate tests (if required)
    3. Update dependencies
    4. Run tests
    5. Fix errors
    6. 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