Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing students' code in Jupyter with a unittest

I'd like my students to be able to check their code as they write it in a Jupyter Notebook by calling a function from an imported module which runs a unittest. This works fine unless the function needs to be checked against objects which are to be picked up in the global scope of the Notebook.

Here's my check_test module:

import unittest
from IPython.display import Markdown, display

def printmd(string):
    display(Markdown(string))

class Tests(unittest.TestCase):

    def check_add_2(self, add_2):
        val = 5
        self.assertAlmostEqual(add_2(val), 7)

    def check_add_n(self, add_n):
        n = 6
        val = 5
        self.assertAlmostEqual(add_n(val), 11)


check = Tests()
def run_check(check_name, func, hint=False):
    try:
        getattr(check, check_name)(func)
    except check.failureException as e:
        printmd('**<span style="color: red;">FAILED</span>**')
        if hint:
            print('Hint:',  e)
        return
    printmd('**<span style="color: green;">PASSED</span>**')

If the Notebook is:

In [1]: def add_2(val):
            return val + 2

In [2]: def add_n(val):
            return val + n

In [3]: import test_checks

In [4]: test_checks.run_check('check_add_2', add_2)
        PASSED

In [5]: test_checks.run_check('check_add_n', add_n)
        !!! ERROR !!!

The error here is not suprising: add_n doesn't know about the n I defined in check_add_n.

So I got to thinking I could do something like:

In [6]: def add_n(val, default_n=None):
            if default_n:
                n = default_n
            return val + n

in the Notebook, and then passing n in the test:

    def check_add_n(self, add_n):
        val = 5
        self.assertAlmostEqual(add_n(val, 6), 11)

But this is causing me UnboundLocalError headaches down the line because of the assignment of n, even within an if clause: this is apparently stopping the Notebook from picking up n in global scope when it's needed.

For the avoidance of doubt, I don't want insist that n is passed as an argument to add_n: there could be many such objects used but not changed by the function being tested and I want them resolved in the outer scope.

Any ideas how to go about this?

like image 901
xnx Avatar asked Oct 18 '22 00:10

xnx


1 Answers

You can import __main__ to access the notebook scope:

import unittest
from IPython.display import Markdown, display

import __main__


def printmd(string):
    display(Markdown(string))

class Tests(unittest.TestCase):

    def check_add_2(self, add_2):
        val = 5
        self.assertAlmostEqual(add_2(val), 7)

    def check_add_n(self, add_n):
        __main__.n = 6
        val = 5
        self.assertAlmostEqual(add_n(val), 11)


check = Tests()
def run_check(check_name, func, hint=False):
    try:
        getattr(check, check_name)(func)
    except check.failureException as e:
        printmd('**<span style="color: red;">FAILED</span>**')
        if hint:
            print('Hint:',  e)
        return
    printmd('**<span style="color: green;">PASSED</span>**')

This gives me a PASSED output.


This works because when you execute a python file that file is stored in sys.modules as the __main__ module. This is precisely why the if __name__ == '__main__': idiom is used. It is possible to import such module and since it is already in the module cache it will not re-execute it or anything.

like image 65
Bakuriu Avatar answered Oct 21 '22 02:10

Bakuriu