OneZoom / OZtree

OneZoom Tree of Life Explorer
Other
88 stars 18 forks source link

Unit tests for controller functions #410

Closed hyanwong closed 1 year ago

hyanwong commented 3 years ago

Hi @lentinj - I was thinking of coding up some unit tests for controller functions, such as sponsor_node.py, e.g. to check that the controller functions return the correct variables, without having to go through all the tedious browser-emulation stuff.

I wondered if this was a sensible approach, and if so, how this could be fitted into the unit test functions you set up in https://github.com/OneZoom/OZtree/tree/3.5/tests/unit? In particular, how I could call e.g. sponsor_node as if it were run by the main web2py app, so it has access to variables such as request and db? Any help gratefully accepted.

lentinj commented 3 years ago

The general question to ask yourself is: "Is the interface I'm testing stable?", i.e. is any likely future refactor going to keep this interface, or is it likely to be thrown away, making the tests useless in the process? Writing tests that test a public API is a pretty safe bet (any refactoring isn't going to get rid of the API), anything else involves predicting the future somewhat.

In terms of a controller, this amounts to how stable the URL is, and how tied the output structure is to the template you use. If the tests break any time you reorganise the view they're kinda useless. The aim of a controller tends to be grouping together several disparate bits of logic together so their result can be funnelled through a view and out over a single web request—the OZ homepage is a prime example of this, and an easy way to make brittle tests would be to test the whole structure returned from the controller, so any time you add to it all the tests break. If you want to change this controller from returning 25 to 30 then you have to rewrite all the tests to remove "first25".

Having reasonably high-level functions in the model (like in the sponsorship module) is a good way of avoiding this I think, and means you can test that functionality without worrying about how the controller plugs them together, now and in the future. In this case I'd be tempted to have a "get_common_name_by_id_or_ott()", and stuff as much logic as you can from the controller into that, and test that function. Then any test for the controller is simply "does the common_name part of the output get populated"? Since other edge cases can be tested upstream.

As for whether this is better than inspecting the browser output, as you've noticed it's hard to make tests on HTML output that don't end up being too brittle, it'd certainly be easier to test that the controller returns only 25 items, rather than counting them in the view (and having to keep up-to-date with what constitutes a species item in the HTML). Again, it depends what interface is most stable.

So, try to farm out logic into smaller chunks to test if you can, but at some point you will have to test the controller too, and it's not a bad idea.

As for running it as-if in a web2py app, there's several relevant parts:

...after all this the code can refer to db and request.args happily. Your test can set args, call the function, see what comes back.

jrosindell commented 1 year ago

See also #498 which is a duplicate

lentinj commented 1 year ago

We have several examples of how to do this now, as well as a helper in util.call_controller, so I think this can be closed.