How To Easily And Confidently Implement Unit Tests In Python



Original Source Here

How To Easily And Confidently Implement Unit Tests In Python

Do you want to sleep better at night knowing that your code isn’t going to break? Then this article is for you.

Photo by Brett Jordan on Unsplash

What are unit tests, why unit testing is important, what are the best practices for unit tests, and how do I implement unit tests in Python? If you’d like to know the answers to these questions and more, read on.

What are tests?

Testing is a simple and intuitive concept. You write tests parallel to your primary code to ensure that it works the way you expect it to. Everybody tests their code in some way or another — but there are better and worse ways to do so.

Most people will run quick tests in the terminal, or with a mashed-up mix of assert statements and print statements. I’m not saying don’t do this, but there are more effective ways to test your code which I’ll go on to explain shortly. But first, let me convince you that testing your code is essential.

Why use tests?

First of all, if you are writing tests alongside your code, it will make you think about your code on a much deeper level. It will make you think much more about the inputs, outputs, and objectives of the piece of code you are writing. This will encourage you to write much more efficient code from the beginning.

It can save you endless hours of debugging. Think about writing tests as an investment in your time (and stress levels). As you write code, you write tests alongside it. If something goes pear-shaped, your investment into these tests will reward you with a big arrow pointing towards the problem. This is especially true when it comes to long and complicated functions.

Tests are also replicable. In the same way that you copy and paste a function from one project to another, you can do the same with tests. Your functions and tests are like Batman and Robin.

You can even write the tests before your functions! There’s a school of thought called test-driven-development (TDD) which suggests that you should write functions before tests. Whether that is a good idea is a very contentious debate and one that I don’t plan on getting involved in.

How to use tests?

Unit v integration tests

First of all, we need to discuss the two main types of tests. The main test type that you’ll come across is the unit test. This is a test of a specific unit or component — most commonly a function. Integration testing is testing how all these components fit together. The following examples will focus on unit testing.

So, let’s start with a basic example of a standalone unit test. Suppose we have the function below:

def add(x,y):
return x + y

Then an example of a unit test for this would be:

assert add(2,4) == 6, "Should be 6"

The assert keyword lets you test if a condition in your code returns True, if not, the program will raise an AssertionError. Be careful with the parenthesis here, as assert is a statement.

If you run this in your Python terminal, nothing will happen as 2 + 4 does in fact equal 6. Try it again, changing the 6 to a 7, and, as promised, you’ll get an AssertionError.

Created by Author.

As you can see it shows the error message that followed the assert statement.

We can be much more efficient and organised with our testing though.

Test cases, test suites and test runners

Let me introduce you to a few concepts.

Firstly, Test Cases. A test case is a specific test of a case or a response. The assert statement is an example of a test case. We are checking that in the case that 2 + 4 is inputted, then we receive the answer of 6.

If we group together a lot of test cases, we get a Test Suite. Often, it makes sense to add a lot of similar cases together.

When we run our test cases and our test suites, we need an organised and efficient way of doing it. This is where we use a Test Runner. Test runners orchestrate the execution of the tests and make life a lot easier for us.

There are many test runners, but my favourite and the one built into python is Unittest. That’s what we will work with today.

Using Unittest

Unittest has a few rules that you must follow. It is simple, elegant and easy to use once you get your head around it.

Firstly, you have to put all your tests into classes as methods. Once you do this you replace the assert keyword with special assertion methods inherited from the unittest.TestCase class.

So, here’s an example using the same example we already looked at. I’ve created a new file there called “tests.py”, which is a standard convention. I’ve stored the add function in a folder called functions at the same level as test.py.

Created by Author.
import unittestimport functions
class TestAdd(unittest.TestCase): def test_add(self): self.assertEqual(functions.add(2, 4), 6)
if __name__ == '__main__':
unittest.main()
  1. Firstly, we have to import unittest as standard.
  2. Create a class called TestAdd that inherits from the TestCase class.
  3. Change the test functions into methods.
  4. Change the assertions to use the self.assertEqual() method in the TestCase class. A full list of available methods can be seen below.
  5. Change the command-line entry point to call unittest.main()
Full list of unittest assert methods — https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertTrue

So now, if you run test.py in the terminal you should see this.

Created by Author.

Each dot above the dashed line represents a test that’s been run. If this test threw up an error, it would be replaced with an E or F if it failed.

So if we replace the 6 with a 7 then we get this:

Created by Author
Created by Author

Of course, we only have one test here so we already know where it was failing. If we had a lot more it would be very easy to see where it had failed as it’s very specific.

How to write good tests?

Name your tests clearly — and don’t forget to call them tests.

This test won’t run.

class TestAdd(unittest.TestCase):     def add_test(self):          self.assertEqual(functions.add(4, 2), 7)

Test methods have to begin with ‘test’. ‘test-add’ will run but ‘add test’ will not. If you define a method that doesn’t begin with ‘test’ it will automatically pass – because it never ran. So it might as well not be there at all. Actually, it’s a lot worse to have a test you think has passed than a test that doesn’t exist. It can throw off your bug fixing. Also, don’t be scared of a long name, be specific. It makes finding the bug a lot easier.

Start with simple intuitive tests and build up

Start with the tests that spring to mind first. These should make sure that the primary objective of your function is correct. Once these tests pass, then you can think about more complicated tests. Don’t get complicated until you ensure the basic functionality is correct.

Edge cases and crossing the boundaries

I like to start and think about the edge cases. Let’s take the example of working with numbers. What happens if we input negatives? Or floats? Or a boundary number like zero. Zeros love breaking code so it’s always good to have a test for that. So let’s chuck some more examples in there.

class TestAdd(unittest.TestCase):     def test_add(self):          self.assertEqual(functions.add(4, 2), 7)          self.assertEqual(functions.add(-1, 1), 0)          self.assertEqual(functions.add(-1, -1), -2)          self.assertEqual(functions.add(0, -1), -1)

and voila, our code seems to be good:

Each test should be independent

Tests should never depend on each other. Unittest has in-built functionality to prevent you from doing this. The setUp() and tearDown() methods allow you to define instructions that will be executed before and after each test method. I say this is because Unittest doesn’t guarantee that your tests will run in the order you have specified.

Avoid using Assert.IsTrue

It simply doesn’t give you enough information. It will tell you nothing more than if the value was true or false. If you use the assertEqual method as we did earlier you’ll get much more information.

AssertionError: 6 != 7

Is much easier to debug than:

Expected True, but the actual result was False

Sometimes your tests will still miss things — that’s okay.

Just go back and add in a new test so you don’t miss it next time.

Let’s look at this example of a function that rounds a number and adds ten. Nothing crazy.

def round_plus_ten(x):     x = round(x) + 10     return x

and this test suite.

class TestRound(unittest.TestCase):     def test_round(self):          self.assertEqual(functions.round_plus_ten(4.3), 14)          self.assertEqual(functions.round_plus_ten(4.7), 15)          self.assertEqual(functions.round_plus_ten(4.5), 15)

From looking at this (if you didn’t already know the ins ad outs of the round method) you’d expect all the tests to pass. They don’t.

Created by Author
Created by Author

You can see here (thanks to the usefulness of the test runner) that rounding 4.5 does not equal five. Thus when we add ten it does not equal 14. The round method rounds down at the margin instead of up. These are the little bugs that can sometimes throw off a whole program, and as I said at the start, you wouldn’t even think of them.

This example highlights two things.

1 — You’ll never think of all the ways your program can fail, but testing improves your chances.

2 — If you miss something, go back to your test file and write a new assertion for this case. This means that you won’t miss it again in the future (including any other projects you copy the function and test file over to).

A good test class is as useful in the future as a good function.

AI/ML

Trending AI/ML Article Identified & Digested via Granola by Ramsey Elbasheer; a Machine-Driven RSS Bot

%d bloggers like this: