Pesquisa de site

Ensine Python com Jupyter Notebooks


Com Jupyter, PyHamcrest e um pouco de fita adesiva de um equipamento de teste, você pode ensinar qualquer tópico de Python que seja passível de teste de unidade.

Algumas coisas sobre a comunidade Ruby sempre me impressionaram. Dois exemplos são o compromisso com os testes e a ênfase em facilitar o início. O melhor exemplo de ambos é Ruby Koans, onde você aprende Ruby corrigindo testes.

Com as ferramentas incríveis que temos para Python, poderemos fazer algo ainda melhor. Pudermos. Usando Jupyter Notebook, PyHamcrest e apenas um pouco de código semelhante a fita adesiva, podemos fazer um tutorial que inclui ensino, código que funciona e código que precisa ser consertado.

Primeiro, um pouco de fita adesiva. Normalmente, você faz seus testes usando algum bom executor de testes de linha de comando, como pytest ou virtude. Normalmente, você nem mesmo o executa diretamente. Você usa uma ferramenta como tox ou nox para executá-lo. No entanto, para o Jupyter, você precisa escrever um pequeno equipamento que possa executar os testes diretamente nas células.

Felizmente, o equipamento é curto, senão simples:

import unittest

def run_test(klass):
    suite = unittest.TestLoader().loadTestsFromTestCase(klass)
    unittest.TextTestRunner(verbosity=2).run(suite)
    return klass

Agora que o arnês está pronto, é hora do primeiro exercício.

No ensino, é sempre uma boa ideia começar aos poucos, com um exercício fácil para aumentar a confiança.

Então, por que não consertar um teste realmente simples?

@run_test
class TestNumbers(unittest.TestCase):
    
    def test_equality(self):
        expected_value = 3 # Only change this line
        self.assertEqual(1+1, expected_value)
    test_equality (__main__.TestNumbers) ... FAIL
    
    ======================================================================
    FAIL: test_equality (__main__.TestNumbers)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-7-5ebe25bc00f3>", line 6, in test_equality
        self.assertEqual(1+1, expected_value)
    AssertionError: 2 != 3
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
    
    FAILED (failures=1)

Alterar apenas esta linha é um marcador útil para os alunos. Mostra exatamente o que precisa ser mudado. Caso contrário, os alunos poderiam corrigir o teste alterando a primeira linha para return.

Neste caso, a correção é fácil:

@run_test
class TestNumbers(unittest.TestCase):
    
    def test_equality(self):
        expected_value = 2 # Fixed this line
        self.assertEqual(1+1, expected_value)
    test_equality (__main__.TestNumbers) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
    
    OK

Rapidamente, porém, as asserções nativas da biblioteca unittest se mostrarão deficientes. Em pytest, isso é corrigido reescrevendo o bytecode em assert para ter propriedades mágicas e todos os tipos de heurísticas. Isso não funcionaria facilmente em um notebook Jupyter. É hora de descobrir uma boa biblioteca de asserções: PyHamcrest:

from hamcrest import *
@run_test
class TestList(unittest.TestCase):
    
    def test_equality(self):
        things = [1,
                  5, # Only change this line
                  3]
        assert_that(things, has_items(1, 2, 3))
    test_equality (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_equality (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-11-96c91225ee7d>", line 8, in test_equality
        assert_that(things, has_items(1, 2, 3))
    AssertionError: 
    Expected: (a sequence containing <1> and a sequence containing <2> and a sequence containing <3>)
         but: a sequence containing <2> was <[1, 5, 3]>
    
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.004s
    
    FAILED (failures=1)

PyHamcrest não é bom apenas em afirmações flexíveis; também é bom para limpar mensagens de erro. Por causa disso, o problema é fácil de ver: [1, 5, 3] não contém 2 e, além disso, parece feio:

@run_test
class TestList(unittest.TestCase):
    
    def test_equality(self):
        things = [1,
                  2, # Fixed this line
                  3]
        assert_that(things, has_items(1, 2, 3))
    test_equality (__main__.TestList) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK

Com Jupyter, PyHamcrest e um pouco de fita adesiva de um equipamento de teste, você pode ensinar qualquer tópico de Python que seja passível de teste de unidade.

Por exemplo, o seguinte pode ajudar a mostrar as diferenças entre as diferentes maneiras pelas quais o Python pode remover espaços em branco de uma string:

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 19, in test_end_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string ending with 'world' was '  hello world  '
    
    
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 14, in test_start_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string starting with 'hello' was '  hello world  '
    
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.006s
    
    FAILED (failures=2)

Idealmente, os alunos perceberiam que os métodos .lstrip() e .rstrip() farão o que eles precisam. Mas se não o fizerem, tente usar .strip() em todos os lugares:

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
    
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 19, in test_end_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string starting with '  hello' was 'hello world'
    
    
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 14, in test_start_strip
        assert_that(result,
    AssertionError: 
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string ending with 'world  ' was 'hello world'
    
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.007s
    
    FAILED (failures=2)

Eles receberiam uma mensagem de erro diferente que mostra que muito espaço foi removido:

source_string = "  hello world  "

@run_test
class TestList(unittest.TestCase):
    
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))

    def test_start_strip(self):
        result = source_string.lstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))

    def test_end_strip(self):
        result = source_string.rstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... ok
    test_start_strip (__main__.TestList) ... ok
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.005s
    
    OK

Em um tutorial mais realista, haveria mais exemplos e mais explicações. Essa técnica de caderno com alguns exemplos que funcionam e outros que precisam ser corrigidos pode funcionar para um ensino em tempo real, uma aula em vídeo ou até mesmo, com muito mais prosa, um tutorial que o aluno pode fazer sozinho.

Agora vá lá e compartilhe seu conhecimento!

Artigos relacionados: