Example code - Unit test
I decided to make some unit tests for my prime number generator, I used as an example last week, see Example code - Classes I make two files - one file containing the class PrimeGenerator.py and one file containing the tests, test_PrimeGenerator.py. The files are supposed to be in the same folder. I will not show the code from last week in PrimeGenerator.py, if you want to see the code, click the above link.
This is my unit test file. I want to test the following:
- It can generate different series of primes in a not-ascending order (i.e 10, 20, 15)
- It can figure out if a number is a prime, especially around 0.
- It reacts correctly on wrong (nonsense) input
File: test_PrimeGenerator.py
import pytest from PrimeGenerator import PrimeGenerator # Parametric test of the generator part @pytest.mark.parametrize("x, y", [(10,(2,3,5,7)), (20,(2,3,5,7,11,13,17,19)), (15,(2,3,5,7,11,13))]) def test_generator(x, y): result = tuple(PrimeGenerator(x)) assert result == y, "Generator test" # Parametric test of the giving illegal data to the generator @pytest.mark.parametrize("x", [1.3, 6.5, 'Cat', [1,2,3,4], {1,2,3,4}, {1:1, 2:2, 3:3, 4:4}]) def test_generator_illegal(x): with pytest.raises(ValueError): PrimeGenerator(x) # Parametric test of the isprime part @pytest.mark.parametrize("x, y", [(-3, False),(-2, False),(0, False),(1, False),(2, True),(3, True),(4, False),(5, True),(100, False),(47, True)]) def test_isprime_compute(x, y): assert PrimeGenerator().isprime(x) == y, "isprime test" # Parametric test of the giving illegal data to isprime @pytest.mark.parametrize("x", [1.3, 6.5, 'Cat', [1,2,3,4], {1,2,3,4}, {1:1, 2:2, 3:3, 4:4}]) def test_isprime_illegal(x): with pytest.raises(ValueError): PrimeGenerator().isprime(x)
When running the unit tests with pytest test_PrimeGenerator.py, I got the following problems.
FAILED test_PrimeGenerator.py::test_generator_illegal[1.3] - Failed: DID NOT RAISE <class 'ValueError'> FAILED test_PrimeGenerator.py::test_generator_illegal[6.5] - Failed: DID NOT RAISE <class 'ValueError'> FAILED test_PrimeGenerator.py::test_generator_illegal[x3] - TypeError: int() argument must be a string, a bytes-like ... FAILED test_PrimeGenerator.py::test_generator_illegal[x4] - TypeError: int() argument must be a string, a bytes-like ... FAILED test_PrimeGenerator.py::test_generator_illegal[x5] - TypeError: int() argument must be a string, a bytes-like ... FAILED test_PrimeGenerator.py::test_isprime_illegal[1.3] - Failed: DID NOT RAISE <class 'ValueError'> FAILED test_PrimeGenerator.py::test_isprime_illegal[6.5] - Failed: DID NOT RAISE <class 'ValueError'> FAILED test_PrimeGenerator.py::test_isprime_illegal[Cat] - TypeError: '<=' not supported between instances of 'str' a... FAILED test_PrimeGenerator.py::test_isprime_illegal[x3] - TypeError: '<=' not supported between instances of 'list' a... FAILED test_PrimeGenerator.py::test_isprime_illegal[x4] - TypeError: '<=' not supported between instances of 'set' an... FAILED test_PrimeGenerator.py::test_isprime_illegal[x5] - TypeError: '<=' not supported between instances of 'dict' a...
Despite my best effort in making the PrimeGenerator class, I overlooked something, not in the core code, but in the validation of input. The truth is that I did not think of my test_generator_illegal unit test to begin with, but when I made the test_isprime_illegal and it failed, then I realized the other input validation problem. This just shows that is it valuable to make the unit tests, even with the most stupid and inane test you can think of.
Having fixed my code issues, I here present the new PrimeGenerator code.
#!/usr/bin/env python3 # Prime number generator class PrimeGenerator: # Class varible, known primes in consecutive order, can be extended, but must contain these knownprimes = [2, 3] # Highest tested number for prime highesttested = 3 # Instatiation def __init__(self, number=None): if number is not None: if not isinstance(number, int): # New code raise ValueError("Integer expected") # New code self.target = number # Initializing iteration def __iter__(self): if self.target is None: raise ValueError("No number specified") self.pos = 0 return self # Find next prime def __next__(self): # Can we use the list of known primes to find the next? if self.pos < len(self.knownprimes): nextprime = self.knownprimes[self.pos] if nextprime >= self.target: raise StopIteration self.pos += 1 return nextprime # No, start computing the next prime while self.target > PrimeGenerator.highesttested+1: PrimeGenerator.highesttested += 1 if self._isprime(PrimeGenerator.highesttested): self.knownprimes.append(PrimeGenerator.highesttested) self.pos += 1 return self.highesttested raise StopIteration # Private method for identifying a prime def _isprime(self, number): factor = 0 pos = 0 while factor*factor <= number: # find next potential factor either in known primes or odd numbers above last known prime if pos < len(self.knownprimes): factor = self.knownprimes[pos] pos += 1 else: factor += 2 # test if it truly is a factor if number % factor == 0: return False return True # It is nice be able to ask if an number is a prime def isprime(self, number=None): if number is None: number = self.target if not isinstance(number, int): # New code raise ValueError("Integer expected") if number in PrimeGenerator.knownprimes: return True if number <= PrimeGenerator.highesttested: return False return self._isprime(number) if __name__ == "__main__": # Small testing for i in PrimeGenerator(1000): print(i) print(PrimeGenerator().isprime(1000000016531)) # True print(PrimeGenerator().isprime(1000000016521)) # False # Big prime, don't try # 999296950101072104250052714631