Anvil Community Forum

[Wiki] Best practices: Test-driven development with Anvil

Temporary disclaimer about stale content

In the What not to do paragraph, the rule Do not return database row objects to the client is the one that I follow most religiously. With the exception of my oldest apps, I have zero linked tables. I even refactored a few old apps not to use linked tables.

Instead of using tables with linked rows, I do something like:

  • Do one search to find all the rows from the first table
  • Create a list of dictionaries with it
  • Scan the list of dictionaries and find all the ids of the rows of the second table that I need
  • Do a second search to find all the rows from the second table
  • Create a dictionary of dictionaries with it

This way I have 1 access to the database per table instead of 1 access for the first search plus 1 access for every linked row, plus 1 access for every simple object row plus etc. etc.
In other words, this way may be hundreds of times faster than using the Anvil app_tables.

BUT…

The accelerated tables have just been released , and they do what I am doing, but they do it better: they allow you to specify which columns to cache, including simple objects, including linked tables, specifying which columns of each linked table, down to multiple levels.

I don’t really know how my approach will change, but I’m pretty sure my next app will use linked tables.

I don’t know it because I haven’t played with them yet (other than some meaningless tests) and because I have the feeling that the accelerated tables are just the beginning of more goodies that are coming up in the near future.

After my first few apps using the new accelerated tables, I will update this post.


For a long time I have been looking for a way to create apps in a consistent, testable and maintainable way.

I think I have found one, I have used it while developing my latest apps and I thought I would describe it here in the forum, hoping that it will help someone.


Introduction

In some of my apps most of the action happens on the forms, in some the action happens on client modules, in some on server modules. With Anvil it is so easy to create an app that you don’t really need to be consistent. You can be messy, do what you feel is right at the moment, and it is going to work. Maybe it will be less maintainable, but it will work.

I don’t like being messy and I don’t like to think about the right approach every time I start a new app, but I could never identify the best pattern until recently. Now I found one that has been great for the last few apps.

This is not the best practice for everybody, but it has been for me for some time and I think it will be useful to others. I will try to describe it as well as I can in this wiki post. Over time I will try to improve it and I may also add examples. I’m going to publish it before it’s complete, otherwise I would never publish it. So feel free to correct and add to it, as long as your contributions don’t change the basic idea.


What not to do
  • In client side modules:
    • Do not interact with form components
  • In forms:
    • Do not manage app logic
    • Do not call server side functions
    • Do not use dictionaries for item or lists of dictionaries for items
  • In server side modules:
    • Do not manage app logic
    • Do not return database row objects to the client
  • Do not fix code in Globals if you find out that something does not work while working on the UI
  • Do not add code in Globals if you find out that something is missing while working on the UI

What to do
  • In client side modules:
    • Create one client side module called Globals with the definition of several classes, often with one master class and all the other classes hierarchically related to it
    • Derive all the classes involved in databinding from AttributeToKey (see below)
    • Create a @property for every class attribute that will be used in databinding. Properties of classes derived from AttributeToKey behave as dictionaries and play nicely with databinding
    • Create one global variable Globals.all_data, the singleton instance of the top level class
    • Globals.all_data contains all the data used by the client
    • Globals.all_data has methods that call server functions to load data from and store data to database and access other services
    • Try to create only one method on the master class that writes to and one that reads from the database. Not a black and white rule, sometimes it does make sense to create more methods for loading and storing different types of data
    • In order to save to and load from the database, the classes have a to_json method to serialize the current instance and a from_json class method to deserialize and create a new instance
  • In forms:
    • Use Globals.all_data to read and write all the required information
    • Use Globals.all_data members to manage databinding
  • In server side modules:
    • Try to create only one function to read all kinds of data from the database, convert to json and pass to the client. Again, not a black and white rule
    • Try to create only one function to write all kinds of data to the database
    • Include some logic to protect against malicious users. In simple cases this has nothing to do with business logic and can be done by checking that the user is signed in
  • If you find out that something does not work or is missing while working on the UI, go to the test suite and add a test that reproduces the same problem, then work on the code until the test passes, then go back working on the UI

What are the advantages?
  • Removing all the logic from the form prevents the forms from having logic bugs. The only problems in the form code will affect the interface
  • Removing all the logic from the server modules prevents the server modules from having logic bugs. All the code in the server modules is easily testable by testing Globals
  • Since Globals has no interactions with forms, with databases or with any other services, it’s easily testable offline. Well, not really offline, often an uplink connection is required to test the interactions with the database and other services, but it’s easy to test locally without running the app on the Anvil server
  • It is possible to import the singleton in the app console and explore all the app data while the app is running and test the logic with from AppPackageName.Globals import all_data (where AppPackageName is defined at the bottom of the Settings - Dependencies page)

What are the disadvantages?
  • I have the feeling that sometimes the databinding fails silently instead of showing an error.

    Anvil does a good job at showing problems with databinding when the databinding is done with a dictionary, while when I make mistakes in the databinding I see nothing. I don’t know yet when this happens, I’m not even sure it does happen. I will keep my eyes open and, maybe it’s just matter of raising the correct exception in AttributeToKey

  • Managing the logic on the client side could be impossible if modules unavailable on the client are required

  • Managing the logic on the client side may expose some aspects of your logic that should be kept secret


AttributeToKey

I mentioned earlier that all the classes involved in databinding should be derived from AttributeToKey.
This is the definition of the AttributeToKey class:

class AttributeToKey:
    def __getitem__(self, key):
        try:
            return self.__getattribute__(key)
        except AttributeError:
            raise KeyError(str(key))

    def __setitem__(self, key, item):
        self.__setattr__(key, item)

    def get(self, key, default=None):
        try:
            return self.__getattribute__(key)
        except AttributeError:
            return default

Classes derived from this AttributeToKey will behave like dictionaries and will play nicely with databinding when the component expects a dictionary.

For example this is how this class will behave:

class Candy(AttributeToKey):
    def __init__(self, color, shape):
        self.color = color
        self.shape = shape

candy = Candy('red', 'round')
print(candy.color, candy.shape)
> red round
print(candy['color'], candy['shape'])
> red round

And this is how this class can be used with databinding on a DataGrid:

image

Summary: often Anvil assumes the databinding is managed with a dictionary and it is possible to replace self.item['color'] with self.item.color. But in some cases, like with a DataGrid, the dictionary key is required, so it would be impossible to target a class member. The AttributeToKey base class allows to use databinding with any class by behaving like a dictionary and returning instance member values.


Setup the test suite
  • Create the app, add a client module called Globals, add a server module
  • Clone the app to your computer
  • Create the file Test\test.py with a test suite to test all the classes that don’t exist yet. Here is what an app looks like at this point on my PC, after opening it with PyCharm:
    image
  • Push back to the server. Here is what the commit looks like after the push:

    image

See here for details about how to setup a reliable testing environment: Setting up an environment for testing modules locally


Test-driven vs Messy development

Messy development: create the app, skip the test, hope that it works, hope you don’t need any maintenance
Creating the UI with Anvil is so easy that you often start by creating the UI, then writing the code to make the UI happy. Then, eventually, one day, perhaps, maybe, maybe not, you will try to create some tests. The logic is entangled with the forms and it’s split half on the client and half on the server, and you will give up the testing. Then bugs will start showing up, and you will be scared because you don’t know what changing that line of code will break.

Test-driven development: code your tests rather than test your code and feel safe that everything is tested and any future change will not break the app
Don’t waste time setting up a data grid before you know whether you need a data grid. The same applies to all the forms and UI components.
When you start working on an app you know what you want your forms to do, but don’t know the details. Often you need to completely redesign a form because you didn’t think about certain details of the workflow until you tried to write the code in the form.

With test-driven development, you first create a test to test your classes that do everything your forms should do, but without the UI and without the classes. Those tests will obviously fail, complaining that the classes don’t exist.

Then create the classes and develop them untili the tests stop failing.

When all your tests succeed, you are ready to work on the UI: commit and push to the Anvil server. At this point you know how the app logic works, you know your classes and building the forms will be a breeze.

If you realize that your classes miss something, restart from the test: first add a test for whatever was missing, then add it to Globals, then work on the form.


Example of test.py

Here is what the imports look like:

from unittest import TestCase

# Server is required for the uplink connection
import anvil.server

# The user service can be useful to sign in and find database rows linked 
# to the current user.
import anvil.users

# The tables can be used to check test results or to get the user row.
from anvil.tables import app_tables

# This import allows the decorated server callable to run on the uplink.
# Without it, they will run inside the app.
# With this import the server callable will run in a thread different from the
# test. Set a breakpoint on the server callable function to debug.
import server_code.ServerModule

# Different statements to import Globals when running on uplink vs on server.
# Importing Globals will instantiate the global variable that will contain all 
# the data used on the client.
# For this example let's make "boxes" that global variable.
if anvil.server.context.type == 'uplink':
    # running tests on pc
    from client_code.Globals import *
else:
    # running on Anvil server
    from .Globals import *

Here is what tests look like:

class Tests(TestCase):
    def test_1(self):
        # test functionalities that don't need to save or read from database
        box1 = boxes.add_box('box 1')
        candy1 = box1.add_candy('red', 'round')
        candy2 = box1.add_candy('red', 'square')
        self.assertEqual(candy1.color, candy2.color)
        self.assertEqual(2, box1.n_candies)

    def test_2(self):
        # test interactions with the database
        anvil.server.connect(r'server_<uplink-key>')
        anvil.users.force_login(app_tables.users.get(email='<test-email>'))

        box1 = boxes.add_box('box 1')
        candy1 = box1.add_candy('red', 'round')
        candy2 = box1.add_candy('red', 'square')
        boxes.save()

        boxes2 = Boxes()
        boxes2.load()
        box1 = boxes2['box 1']
        candy1 = box1.candies[0]
        candy2 = box1.candies[1]
        self.assertEqual(candy1.color, candy2.color)
        self.assertEqual(2, box1.n_candies)

Example of Globals.py, good enough to pass test_1

After creating the tests shown above, you need to create the classes so those tests will succeed. Here is a quick (ironically enough untested) example.

The imports and the AttributeToKey class:

import anvil.server

class AttributeToKey:
    def __getitem__(self, key):
        return self.__getattribute__(key)

    def __setitem__(self, key, item):
        self.__setattr__(key, item)

The Boxes class:

class Boxes(AttributeToKey):
    def __init__(self):
        self.boxes = []

    def add_box(self, name):
        self.boxes.append(Box(name))

The Box class:

class Box(AttributeToKey):
    def __init__(self, name):
        self.name = name
        self.candies = []

    def add_candy(self, candy):
        self.candies.append(Candy(color, shape))

    @property
    def n_candies(self):
        return len(self.candies)

The Candy class:

class Candy(AttributeToKey):
    def __init__(self, color, shape):
        self.color = color
        self.shape = shape

The creation of Global.boxes at the very end of the file:

boxes = Boxes()

Example of Globals.py, good enough to pass test_2

After making sure that test_1 passes, we need to make sure that also test_2 passes. The missing mehtods are Boxes.save() and Boxes.load(), plus all the serialization machinery and the server code with the required callables.

Add to the Boxes class:

    def save(self):
        json_data = [box.to_json() for box in self.boxes]
        anvil.server.call('save', data=json_data)

    def load(self):
        json_data = anvil.server.call('load')
        self.boxes = [Box(box) for box in json_data]

Add to the Box class:

    def to_json(self):
        [...]
        return json

    @classmethod
    def from_json(cls, json):
        box = Box(json['name'])
        for candy_json in json['candies']:
            box.add_candy(Candy.from_json(candy_json))
        return box

The Candy class will get a treatment similar to Box.

The server module will get the save and load callables.


Rendering the form

In my forms the components change the data by setting some properties or calling some methods of Globals.all_data (or one of its descendents), then call the update_interface() form method to re-render the form.

A form that shows the content of a box with a repeating panel for the candies and one for the cookies is structured like this:

class BoxDetails(BoxDetailsTemplate):
    def __init__(self, **properties):
        self.init_components(**properties)

        self.candies.set_event_handler('x-delete-candy', self.delete_candy)
        self.cookies.set_event_handler('x-delete-cookie', self.delete_cookie)

    def form_show(self, **event_args):
        self.box = boxes.get_box_by_[...]([...])
        self.update_interface()

    def delete_candy(self, candy, **event_args):
        self.box.delete_candy(candy)
        self.update_interface()

    def delete_cookie(self, cookie, **event_args):
        self.box.delete_cookie(cookie)
        self.update_interface()

    def update_interface(self, **event_args):
        # initialize the repeating panels
        self.candies.items = self.box.candies
        self.cookies.items = self.box.cookies

In the above code self.box = boxes.get_box_by_[...]([...]) is inside form_show instead of __init__ because I use the routing module from Anvil Extras and I usually want to reload the data when navigating back to the form. (The code snippets in this page don’t show any code related with the routing module.) The [...] parts of that line could be by_name(name), by_id(id), etc.

Re-rendering the whole form containing many components and repeating panels is obviously less efficient than removing one component as shown in this tutorial, but so far I haven’t had any performance problems. Perhaps when the need comes I will do something like update_interface(candies=False, cookies=False, cupcakes=False) to avoid re-rendering repeating panels that have not changed.

The RowCandiesList form looks like this:

class RowCandiesList(RowCandiesListTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)

  def delete_click(self, **event_args):
    self.parent.raise_event('x-delete-candy', candy=self.item)

When the repeating panel is inside a datagrid I use this helper function to make sure the same page is displayed when re-rendering:

def update_data_grid(self, repeating_panel, items):
    grid = repeating_panel.parent
    page = grid.get_page()
    repeating_panel.items = items
    grid.set_page(page)

Minimizing round trips

In the examples above the server functions save and load will save and load all the data.

In real apps this may not be the right thing to do. You may need the full list of boxes without the info of their candies or only one box with all the info about its candies, or both. Or you may have other classes like CupCakes and Cookies, and some forms may need info about one or the other.

Creating one server function per class you will almost certainly end up executing several roundtrips on forms that need different types of data.

The best way to minimize the number of roundtrips is to create one save and one load functions, both able to optionally manage all kinds of data. It is good to also optimize the amount of data sent to the client by creating dictionaries that only include the required columns. Something like:

@anvil.server.callable
def load(list_of_boxes=False, box_name='', candies=False,
         cookies=False, cupcakes=False):

    data = {}

    if list_of_boxes:
        # return the list of boxes for the current user, only the box name
        data['boxes'] = [row['name'] for row in
                         app_tables.boxes.search(user=anvil.users.get_user())]

    if box_name:
        # return detailed information for the specified box
        row = app_tables.boxes.get(user=anvil.users.get_user(), name=box_name)
        data['box'] = {
            'name':     row['name'],
            'candies':  row['candies'],
            'cookies':  row['cookies'],
            'cupcakes': row['cupcakes']
        }

    if candies:
        # return the list of candies for the current user

Similarly, the save function will be able to save different kinds of data, but will only save what is provided:

@anvil.server.callable
def save(boxes=None, candies=None, cookies=None, cupcakes=None):
    if boxes:
        for row in app_table.boxes.search(user=anvil.users.get_user()):
            row.delete()
        for box in boxes:
            app_table.boxes.add_row(user=anvil.users.get_user(), **box)

    if candies:
        [...]

Making it lazy

Most of my classes are lazy: if they have what they need, they do what they need to do. If they don’t have it, they get it once and keep it for later.

This may trigger undesired roundtrips, for example if the form started by calling boxes.load(candies=True, cookies=False) and later used boxes.list_of_cookies. Here there is one roundtrip when load is called, then a second roundtrip when list_of_cookies is called, because the information is not yet available.

I like to monitor all round trips by adding something like this to all server callables:

@anvil.server.callable
def load(boxes=None, box_name='', candies=None, cookies=None, cupcakes=None):
    tokens = []
    if boxes: tokens.append('boxes')
    if box_name: tokens.append(f'box_name={box_name}')
    if candies: tokens.append('candies')
    if cookies: tokens.append('cookies')
    if cupcakes: tokens.append('cupcakes')
    print(f"load({', '.join(tokens)})")

When I open the form that calls both boxes.load(candies=True) and boxes.list_of_cookies, I will see this in the console:

load(boxes)
load(cookies)

So I will change the first line into boxes.load(candies=True, cookies=True). When the next line boxes.list_of_cookies is executed, the data is already loaded and the second round trip will not be executed. The new output will show only one round trip:

load(boxes, cookies)

Working with SQL

I have a dedicated plan, many of my apps use SQL and my uplink scripts don’t have access to SQL.

The workaround is for the server function to call another server function using anvil.server.call() rather than importing it and calling it directly.

I usually have only one SQL query per server function call, so having one function that does the connection and executes the query is not a problem.

In order to test functions that use SQL from the uplink I add a server module called SQL.py containing this:

@anvil.server.callable
def execute_query_and_fetchall(cmd):
    cur = connect_to_postgres()
    cur.execute(cmd)
    return cur.fetchall()

And I have this in the server module:

if anvil.server.context.type == 'uplink':
    # running tests on pc: use anvil.server.call and run on the server
    def execute_query_and_fetchall(cmd):
        return anvil.server.call('execute_query_and_fetchall', cmd=cmd)
else:
    # running on Anvil server: import the function directly
    from Sql import execute_query_and_fetchall

Easy improvements

When all the tests will pass, the classes will have all the properties required by the forms. The properties will be there available to all the forms to use them, whether they need them or not, without bothering creating a dictionary.

For example on the Truck class of one of my apps I have the following properties. The properties have been used first on the test, before they existed, then they have been added to the class Globals.Truck. Now, when I receive an email asking to add the weight of the truck to a form or to a PDF, the property is there. I add a column to a DataGrid, decide whether to use the numerical or the formatted property, and it works. There is no need to add it to a dictionary or to copy code from one form where the weight was used to the form where I need to add it.

    @property
    def estimated_weight(self):
        return base_weight + sum(crate.weight for crate in self.crates)

    @property
    def actual_or_estimated_weight(self):
        return self.actual_weight or self.estimated_weight

    @property
    def actual_or_estimated_weight_fmt(self):
        return fmt(self.actual_or_estimated_weight)

Major edits

In case you already ready this post in the past, here are the major edits since the first version:

  • Added the What are the disadvantages? section
  • Added the Rendering the form section
  • Improved the class AttributeToKey (perhaps this will play more nicely with databinding errors)
  • Added temporary disclaimer about stale content relative to the new accelerated tables
21 Likes