Differences between revisions 4 and 5
Revision 4 as of 2005-09-13 18:11:13
Size: 4023
Editor: raspberry
Comment: Initial rev for tutorial
Revision 5 as of 2005-09-13 19:08:59
Size: 5833
Editor: raspberry
Comment: minor additions and one correction
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
For example, assume you have a prime generator in module prime.py, and you'd want to write a simple test for it (if you don't know it, a prime is a non-negative integer being divisible only by 1 and itself, and the first prime is 2): For example, assume you have a prime generator in module prime.py, and you'd want to write a simple test for it (if you don't know it, a prime is a non-negative integer being divisible only by 1 and itself, and the first prime is 2). Now you should know that it would be probably
wise to start writing test suite before you actually write any code for prime generator itself, but I digress. Let's get on with the test
:
Line 14: Line 15:
That's it - you have a working test suite you can run by giving the test file
as argument for py.test.
That's it - you have a working, albeit far from complete, test suite you can run by giving the test file as argument for py.test. If you saved above snippet to file test_primegen.py, you
could run the tests by simply saying
Line 17: Line 18:
But now you'd like to extend the test a bit further - add a second test for testing primality: ''py.test test_primegen.py''

H
owever, let us assume you'd like to extend the test suite a bit further - add a second test for testing primality using instance method ''isprime()'':
Line 34: Line 37:
  assert pg.isprime(p)         assert pg.isprime(p)
Line 36: Line 39:
  assert not pg.isprime(np)         assert not pg.isprime(np)
Line 54: Line 57:
        assert pg.first_primes(start=5, 4) = [5, 7, 11, 13]         # come to think of it, wrap left side inside list() so that
        # generator methods work as well
        assert list(self.pg.first_primes(start=5, 4)) = [5, 7, 11, 13]
Line 75: Line 80:
Oh, I have one little confession: I did a bit more in addition those two little fixes. For testing statements which return lists it might be advisable to add extra list() around the statement. Thus your test suite works both for list-returning methods as well as those using generators. Or you can circumvent the problem by using method generating expressions - as shown
in the second method. I also added a new (keyword?) parameter ''start'' to first_primes() method
thus specifying where the prime sequence should start. Perhaps stop could be specified as well, thus specifying a range, hmm? Or should you introduce a whole different method for that kind of thing? These are decisions you have to make, and perhaps you realize one very nice aspect of TDD; if you write your tests before the code, you really have to think of your API before you code, and that is a good thing. Imagine fiddling with a method for days, debugging it, optimizing, documenting.. only to realise few days later you don't need the method at all. So, write tests first, if only to get your API correct the first time.
Line 77: Line 86:
''testsuite.run(method)'' or ''testsuite.add(class_instance)'' etc. The idea is neat:
simply, every class starting with ''Test'' and every method starting with ''test_''
''testsuite.run(method)'' or ''testsuite.add(class_instance)'' etc. Well, you're right.
There ''is'' some serious magic going on inside, but the programmer API is very simple.
That is, every class starting with ''Test'' and every method starting with ''test_''
Line 80: Line 90:
is magic, but you don't need to know that... it is so to give you better error messages. is ''very'' magic, but you don't need to know that... things are just that way to give you better error messages.
Line 82: Line 92:
There are more nice features in py.test, which make it a very nice TDD tool: There are many nice features in py.test, which make it a very nice TDD tool. Here I only list some of them:
Line 85: Line 95:
* you can ask py.test abort on first error using -x option * you can ask py.test to abort on first error encountered using -x option
Line 87: Line 97:
* you can start py.test as daemon, constantly monitoring your modules for changes and running tests when they occur * you can start py.test in daemon mode, which will then constantly monitor your modules for changes and running tests automatically when needed

py.test is an alternative, more Pythonic way of writing your tests. The best part is, the overhead for writing unit tests is practically zero!

For example, assume you have a prime generator in module prime.py, and you'd want to write a simple test for it (if you don't know it, a prime is a non-negative integer being divisible only by 1 and itself, and the first prime is 2). Now you should know that it would be probably wise to start writing test suite before you actually write any code for prime generator itself, but I digress. Let's get on with the test:

   1 from prime import PrimeGenerator
   2 
   3 def test_first_primes():
   4     pg = PrimeGenerator()
   5 
   6     assert pg.first_primes(6) = [2, 3, 5, 7, 11, 13]

That's it - you have a working, albeit far from complete, test suite you can run by giving the test file as argument for py.test. If you saved above snippet to file test_primegen.py, you could run the tests by simply saying

py.test test_primegen.py

However, let us assume you'd like to extend the test suite a bit further - add a second test for testing primality using instance method isprime():

   1 from prime import PrimeGenerator
   2 
   3 def test_first_primes():
   4     pg = PrimeGenerator()
   5 
   6     assert pg.first_primes(6) = [2, 3, 5, 7, 11, 13]
   7 
   8 def test_primality():
   9     pg = PrimeGenerator()
  10     
  11     known_primes = (17, 19, 23, 29, 31)
  12     known_nonprimes = (21, 27, 33, 49)
  13     
  14     for p in known_primes:
  15         assert pg.isprime(p)
  16     for np in known_nonprimes:
  17         assert not pg.isprime(np)

It is nice and everything, but now there are two issues. First, if you have many tests, it is bothersome to write pg = PrimeGenerator() in every method - ok, not that bothersome, but in programming, being lazy in particular way is not only elegant, but saves your from errors and other problems in the future, trust me.

The other issue is that if your method isprime() is not working correctly, you see the error, but you don't see what number triggered the error. Let's fix these two problems at next attempt.

   1 from prime import PrimeGenerator
   2 
   3 class TestPrime:
   4 
   5     def setup_class(self):
   6         self.pg = PrimeGenerator()        
   7 
   8     def test_first_primes(self):
   9         # come to think of it, wrap left side inside list() so that
  10         # generator methods work as well
  11         assert list(self.pg.first_primes(start=5, 4)) = [5, 7, 11, 13]
  12 
  13     def _isprime(self, n, expected):
  14         assert self.pg.isprime(n) == expected
  15 
  16     def test_isprime(self):
  17         known_primes = (17, 19, 23, 29, 31)
  18         known_nonprimes = (21, 27, 33, 49)
  19 
  20         for p in known_primes:
  21              yield self._isprime, p, True
  22 
  23         for np in known_nonprimes:
  24              yield self._isprime, np, False            

What happen, you say? Jokes aside, you should notice that first off, you no longer need to setup prime generator instance in every method - setup_class takes care of instantiating needed object. But test_isprime() looks now a tad more exotic - it uses generator expression yield for generating tests on the fly. Now if your prime generator fails, you see the exact error - ie. what argument failed to give correct, expected result.

Oh, I have one little confession: I did a bit more in addition those two little fixes. For testing statements which return lists it might be advisable to add extra list() around the statement. Thus your test suite works both for list-returning methods as well as those using generators. Or you can circumvent the problem by using method generating expressions - as shown in the second method. I also added a new (keyword?) parameter start to first_primes() method thus specifying where the prime sequence should start. Perhaps stop could be specified as well, thus specifying a range, hmm? Or should you introduce a whole different method for that kind of thing? These are decisions you have to make, and perhaps you realize one very nice aspect of TDD; if you write your tests before the code, you really have to think of your API before you code, and that is a good thing. Imagine fiddling with a method for days, debugging it, optimizing, documenting.. only to realise few days later you don't need the method at all. So, write tests first, if only to get your API correct the first time.

You may now wonder - please do! - how py.test knows which methods to run. Well, there has to be some magic going behind the scenes because you don't see any testsuite.run(method) or testsuite.add(class_instance) etc. Well, you're right. There is some serious magic going on inside, but the programmer API is very simple. That is, every class starting with Test and every method starting with test_ is treated specially and considered to be part of unit test suite. Also, assert is very magic, but you don't need to know that... things are just that way to give you better error messages.

There are many nice features in py.test, which make it a very nice TDD tool. Here I only list some of them:

* tests are run in the order you specify them, making tests both deterministic and predictable * you can ask py.test to abort on first error encountered using -x option * running tests will start immediately upon collecting them * you can start py.test in daemon mode, which will then constantly monitor your modules for changes and running tests automatically when needed

So, if you consider giving TDD (Test Driven Development) a try, please try py.test. It makes writing unit tests more fun :*)

More information:


UnitTests

pytest (last edited 2019-05-17 13:35:47 by OliverBestwalter)

Unable to edit the page? See the FrontPage for instructions.