coderWorld
coderWorld

Reputation: 642

How to properly unittest in Python

I have a method which does the following. Question is how do I unit test this method. I am pretty new to this Python unit testing module.

The question and solution are as follows:

Given a string containing of ‘0’, ‘1’ and ‘?’ wildcard characters, generate all binary strings that can be formed by replacing each wildcard character by ‘0’ or ‘1’.
Example :
Input str = "1??0?101"
Output: 
        10000101
        10001101
        10100101
        10101101
        11000101
        11001101
        11100101
        11101101

Solution:

def _print(string, index):
    if index == len(string):
        print(''.join(string))
        return

    if string[index] == "?":

        # replace '?' by '0' and recurse
        string[index] = '0'
        _print(string, index + 1)

        # replace '?' by '1' and recurse
        string[index] = '1'
        _print(string, index + 1)

        # NOTE: Need to backtrack as string
        # is passed by reference to the
        # function
        string[index] = '?'
    else:
        _print(string, index + 1)

# Driver code
if __name__ == "__main__":

    string = "1??0?101"
    string = list(string) #don’t forget to convert to string
    _print(string, 0)

Output:

        10000101
        10001101
        10100101
        10101101
        11000101
        11001101
        11100101
        11101101

Questions:

1. Also, is there a way of returning a list as output instead of printing them out?

2. Which assert test cases are appropriate in this scenario?

3. What would be the best end to end test cases to cover in this case?

4. What could be a better approach of solving this in terms of time and space complexity?

I have tried this which doesn't seem to work:

import unittest
from wildcard import _print
class TestWildCard(unittest.TestCase):

    def test_0_print(self):
        print("Start wildCard _print test: \n")
        result = 111
        self.assertEquals(_print("1?1",0),result,"Results match")

Upvotes: 0

Views: 139

Answers (1)

Grismar
Grismar

Reputation: 31379

Answers:

1: sure, instead of printing something, append the result to a list result.append('some value') and don't forget to initialise the list at the start of your code result = [] and return it once the function is done return result - and probably don't call the function _print, but something like bit_strings.

ad 1: since your function is recursive, you now also need to capture the return value and add it to the result when calling the function recursively, so result += _print(string, index + 1)

2: you should typically think of edge cases and test them separately, or group those together that really test a single aspect of your function. There is no one way to state what the test should look like - if there were, the test framework would just generate it for you.

3: same answer as 2.

Your code becomes:

def bit_strings(s, index):
    result = []

    if index == len(s):
        result.append(''.join(s))
        return result

    if s[index] == "?":
        # replace '?' by '0' and recurse
        s[index] = '0'
        result += bit_strings(s, index + 1)

        # replace '?' by '1' and recurse
        s[index] = '1'
        result += bit_strings(s, index + 1)

        # NOTE: Need to backtrack as string
        # is passed by reference to the
        # function
        s[index] = '?'
    else:
        result += bit_strings(s, index + 1)

    return result


# Driver code
if __name__ == "__main__":
    x = "1??0?101"
    xl = list(x)  #don’t forget to convert to string
    print(bit_strings(xl, 0))

There's more efficient ways of doing this, but I just modified your code in line with the questions and answers.

I've renamed string to s, since string is a bit confusing, reminding others of the type or shadowing the (built-in) module.

As for the unit test:

import unittest
from wildcard import bit_strings


class TestWildCard(unittest.TestCase):
    def test_0_print(self):
        print("Start wildCard _print test: \n")
        # you only had one case here and it's a list now
        result = ['101', '111']
        # user assertEqual, not Equals
        # you were passing in a string, but your code assumed a list, so list() added
        self.assertEqual(bit_strings(list("1?1"), 0), result, "Results match")      

When using an environment like PyCharm, it helps to call the file test<something>.py (i.e. have test in the name), so that it helps you run the unit tests more easily.

Two alternate solutions as requested in comment (one still recursive, just a lot more concise, the other not recursive but arguably a bit wasteful with result lists - just two quickies):

from timeit import timeit


def unblank_bits(bits):
    if not bits:
        yield ''
    else:
        for ch in '01' if bits[0] == '?' else bits[0]:
            for continuation in unblank_bits(bits[1:]):
                yield ch + continuation


print(list(unblank_bits('0??100?1')))


def unblank_bits_non_recursive(bits):
    result = ['']
    for ch in bits:
        if ch == '?':
            result = [s+'0' for s in result] + [s+'1' for s in result]
        else:
            result = [s+ch for s in result]
    return result


print(list(unblank_bits_non_recursive('0??100?1')))

print(timeit(lambda: list(unblank_bits('0??100?1'))))
print(timeit(lambda: list(unblank_bits_non_recursive('0??100?1'))))

This solution doesn't move between lists and strings, as there is no need and doesn't manipulate the input values. As you can tell the recursive one is a bit slower, but I prefer it for readability. The output:

['00010001', '00010011', '00110001', '00110011', '01010001', '01010011', '01110001', '01110011']
['00010001', '01010001', '00110001', '01110001', '00010011', '01010011', '00110011', '01110011']
13.073874
3.9742709000000005

Note that your own solution ran in about 8 seconds using the same setup, so the "improved version" I suggested is simpler, but not faster, so you may prefer the latter solution.

Upvotes: 1

Related Questions