Example code - Unit test: Difference between revisions

From 22113
Jump to navigation Jump to search
(Created page with "I decided to make some unit tests for my prime number generator, I used as an example last week. I make two files - one file containing the class and one file containing the tests. The files are supposed to be in the same folder. Notice I changed the name of the class a bit. <p> '''File:''' primegenerator.py <pre> # Prime number generator class primegenerator: # Class varible, known primes in consecutive order, can be extended, but must contain these knownprimes...")
 
No edit summary
 
Line 1: Line 1:
I decided to make some unit tests for my prime number generator, I used as an example last week.
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 and one file containing the tests. The files are supposed to be in the same folder.
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.
Notice I changed the name of the class a bit.
I will not show the code from last week in ''PrimeGenerator.py'', if you want to see the code, click the above link.
<p>
<p>
'''File:''' primegenerator.py
 
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
<pre>
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)
</pre>
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.
<pre>
<pre>
#!/usr/bin/env python3
# Prime number generator
# Prime number generator
class primegenerator:
 
class PrimeGenerator:
     # Class varible, known primes in consecutive order, can be extended, but must contain these
     # Class varible, known primes in consecutive order, can be extended, but must contain these
     knownprimes = [2, 3]
     knownprimes = [2, 3]
Line 15: Line 69:
     def __init__(self, number=None):
     def __init__(self, number=None):
         if number is not None:
         if number is not None:
             # Don't catch an exception below - it does want we want.
             if not isinstance(number, int):                # New code
            number = int(number)
                raise ValueError("Integer expected")       # New code
         self.target = number                 
         self.target = number                 
      
      
Line 36: Line 90:
             return nextprime
             return nextprime
         # No, start computing the next prime
         # No, start computing the next prime
         while self.target > primegenerator.highesttested+1:
         while self.target > PrimeGenerator.highesttested+1:
             primegenerator.highesttested += 1
             PrimeGenerator.highesttested += 1
             if self._isprime(primegenerator.highesttested):
             if self._isprime(PrimeGenerator.highesttested):
                 self.knownprimes.append(primegenerator.highesttested)
                 self.knownprimes.append(PrimeGenerator.highesttested)
                 self.pos += 1
                 self.pos += 1
                 return self.highesttested
                 return self.highesttested
Line 64: Line 118:
         if number is None:
         if number is None:
             number = self.target
             number = self.target
         if number is None:
         if not isinstance(number, int):               # New code
             raise ValueError("Number expected")
             raise ValueError("Integer expected")
         if number in primegenerator.knownprimes:
         if number in PrimeGenerator.knownprimes:
             return True
             return True
         if number <= primegenerator.highesttested:
         if number <= PrimeGenerator.highesttested:
             return False
             return False
         return self._isprime(number)
         return self._isprime(number)
</pre>


This is my unit test file.<br>
if __name__ == "__main__":
'''File:''' test_primegenerator.py
    # Small testing
<pre>
    for i in PrimeGenerator(1000):
import pytest
        print(i)
from primegenerator import primegenerator


    print(PrimeGenerator().isprime(1000000016531)) # True
    print(PrimeGenerator().isprime(1000000016521)) # False


    # Big prime, don't try
    # 999296950101072104250052714631
</pre>
</pre>

Latest revision as of 11:04, 20 March 2024

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