Testing students' code in Jupyter with a unittest
<p>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.</p>

<p>Here's my <code>check_test</code> module:</p>

<pre><code>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('**&lt;span style="color: red;"&gt;FAILED&lt;/span&gt;**')
        if hint:
            print('Hint:',  e)
        return
    printmd('**&lt;span style="color: green;"&gt;PASSED&lt;/span&gt;**')
</code></pre>

<p>If the Notebook is:</p>

<pre><code>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 !!!
</code></pre>

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

<p>So I got to thinking I could do something like:</p>

<pre><code>In [6]: def add_n(val, default_n=None):
            if default_n:
                n = default_n
            return val + n
</code></pre>

<p>in the Notebook, and then passing <code>n</code> in the test:</p>

<pre><code>    def check_add_n(self, add_n):
        val = 5
        self.assertAlmostEqual(add_n(val, 6), 11)
</code></pre>

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

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

<p>Any ideas how to go about this?</p>

<p>You can <code>import __main__</code> to access the notebook scope:</p>

<pre><code>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('**&lt;span style="color: red;"&gt;FAILED&lt;/span&gt;**')
        if hint:
            print('Hint:',  e)
        return
    printmd('**&lt;span style="color: green;"&gt;PASSED&lt;/span&gt;**')
</code></pre>

<p>This gives me a <code>PASSED</code> output.</p>

<hr>

<p>This works because when you execute a python file that file is stored in <code>sys.modules</code> as the <code>__main__</code> module. This is precisely why the <code>if __name__ == '__main__':</code> 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.</p>

python unit-testing python-3.x jupyter-notebook