Ex 49 - Why do my test return None?

I’m stuck in excersie 49 where I’m trying to test the skip function.

My code looks like this.

parser.py

def peek(word_list):
    
    if word_list:
        word = word_list[0]
        #returns  a either a (noun, verb...)
        return word[0]
    else:
        return None


#checks the first word in a word list
def match(word_list, expecting):
    if word_list:
        #returns and removes first item on wordlist, which is a tuple pair ('noun', 'princess')
        word = word_list.pop(0)

        if word[0] == expecting:
            #returns a tuple pair ('noun', 'princess')
            return word

        else:
            return None
    else:
        return None


def skip(word_list, word_type):
    while peek(word_list) == word_type:
        match(word_list, word_type)

my test is as follows:

import pytest
from ex48 import lexicon, parser

def test_skip():
    word_list = lexicon.scan('princess kill the bear')
    print(f"""
    Word_list: {word_list}
    Peek: {parser.peek(word_list)}
    Match: {parser.match(word_list, 'noun')}
    Skip: {parser.skip(word_list, 'noun')}
    """)
    assert ('noun', 'princess') == parser.skip(word_list,'noun')

I get the following failure when I test:

(lpthw) Kims-MacBook-Pro:ex48 trager$ pytest -s

============================= test session starts ==============================
platform darwin -- Python 3.6.0, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /Users/trager/python/projects/ex48
collected 12 items                                                             

tests/test_ex48.py .......
tests/test_parser.py ....
    >>>Word_list: [('noun', 'princess'), ('verb', 'kill'), ('stop', 'the'), ('noun', 'bear')]
    >>>Peek: noun
    >>>Match: ('noun', 'princess')
    >>>Skip: None
    
F

=================================== FAILURES ===================================
__________________________________ test_skip ___________________________________

    def test_skip():
        word_list = lexicon.scan('princess kill the bear')
        print(f"""
        >>>Word_list: {word_list}
        >>>Peek: {parser.peek(word_list)}
        >>>Match: {parser.match(word_list, 'noun')}
        >>>Skip: {parser.skip(word_list, 'noun')}
        """)
>       assert ('noun', 'princess') == parser.skip(word_list,'noun')
E       AssertionError: assert ('noun', 'princess') == None
E        +  where None = <function skip at 0x10523dc80>([('verb', 'kill'), ('stop', 'the'), ('noun', 'bear')], 'noun')
E        +    where <function skip at 0x10523dc80> = parser.skip

tests/test_parser.py:43: AssertionError
========================= 1 failed, 11 passed in 0.05s =========================

I can see that parser.skip(word_list,‘noun’) should return None.
However I can’t wrap my head around how this is possible.

This is how I understand it:
First word_list runs through the while loop. The first time peek() will return a ‘noun’ which will == my second parameter ‘noun’.
It’ll allow the word_list to run through match() using ‘noun’ as 2nd parameter.
Here it’ll pop the first tuple on the list which is (‘noun’, ‘princess’) and set that to ‘word’. The first value in the tuple is ‘noun’ which is expected so it returns (‘noun’, ‘princess’).
Then the while loop will move onto the next tuple pair (‘verb’, ‘kill’) but this time ‘verb’ does not ‘noun’ so the loop will stop.

But should at least not have returned the first match. Or what I’m not getting?

@ktrager - Try changing return None in the several places it exists to a string about what return statement it is. Like “nested if statement in Match” to see if this makes the failure point easier to identify.

I also think your test will fail when you fix this first issue:

assert ('noun', 'princess') == parser.skip(word_list,'noun')

The two sides of that assertion are not identical. Doesn’t the assert need to be (‘princess’, ‘noun’)?

2 Likes

Hello @ktrager

A little tip on how to concentrate on just the failing test function.

You can call the test with file + function like this.

pytest -v test-parser::test_skip

The ”-v” flag will give you a little bit more information.

1 Like

I don’t know why I didn’t do that as then everything becomes clearer.

Basically I don’t get that there None returned when the while loop hits second work and jumps out of the while loop.

Shouldn’t it return everything until peek(word_list) != word_type?

Parser code:

def peek(word_list):

    print('>>> Inside peek word_list', word_list)
    if word_list:
        word = word_list[0]
        #returns  a either a (noun, verb...)

        print('>>> Inside peek', word)
        return word[0]
    else:
        return 'empty word_list in peek'


#checks the first word in a word list
def match(word_list, expecting):

    if word_list:
        #returns and removes first item on word_list, which is a tuple pair ('noun', 'princess')
        word = word_list.pop(0)
        print(">>> Inside match", word)

        if word[0] == expecting:
            #returns a tuple pair ('noun', 'princess')
            return word

        else:
            return 'nested if statement in match'#None
    else:
        return 'empty word_list in match' #None


def skip(word_list, word_type):
    while peek(word_list) == word_type: #noun == noun
        match(word_list, word_type)

    return 'jumps out of while loop'

Test code

import pytest
from ex48 import lexicon, parser

def test_skip():
    word_list = lexicon.scan('princess kill the bear')
    assert 'jumps out of while loop' == parser.skip(word_list,'noun')

Terminal:

(lpthw) Kims-MBP:ex48 trager$ pytest -s tests/test_parser.py::test_skip
============================= test session starts ==============================
platform darwin -- Python 3.6.0, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /Users/trager/python/projects/ex48
collected 1 item                                                               

tests/test_parser.py >>> Inside peek word_list [('noun', 'princess'), ('verb', 'kill'), ('stop', 'the'), ('noun', 'bear')]
>>> Inside peek ('noun', 'princess')
>>> Inside match ('noun', 'princess') #I expect this to be returned 
>>> Inside peek word_list [('verb', 'kill'), ('stop', 'the'), ('noun', 'bear')]
>>> Inside peek ('verb', 'kill') #verb != noun, jumps out of while loop
.

============================== 1 passed in 0.02s ===============================

Thank you - nice little trick…

I’ve gone through the whole parser code in ex.49 and it’s first when I run the code with a ton of print statements, that I understand whats going on unde the ‘hood’.

I think when I run the different function in isolation it’s when I get confused.
Testing every single chunk from top to bottom, totally makes sense to me until I hit the skip function.
But after the code function the rest of the code only makes sense to me if it’s used in conjunction with the functions below.
Is it too rigid of me to thinking code should be tested top to bottom?

Testing is a big topic with lots of different strategies but what you are doing here really is testing the functions in isolation (a unit test) rather than the flow through them all.

One approach to this is thinking;

  • how will that function be invoked (i.e. is it expecting arguments, what type, how many, … )
  • what does the function do internally that needs to be confirmed or protected?
  • what does the function output (return, yield, etc, in what format, frequency, etc…)

So you can draft tests that consider these things, (and maybe improve the design of your solutions - which opens up a whole other topic of design patterns) to make it more robust.

You’re expecting a list for word_type. What happens if you don’t get a list? Does the design check this or handle the exception? Do you need to test that the object type is a list? What is the likelihood of that function being passed a python type that is not a list? If it’s very low, impossible, ignore it. If not, consider a test. Or refactor to manage the exception.

It’s a very iterative approach as your ideas/hacks become more implemented. But keep in mind, the goals has to be to ensure that any changes you make to your code should only impact the areas you intend to change. Having these unit tests provide you confidence that your changes haven’t impacted the other functions (in theory).