Cara menggunakan python mock exception class

To follow this tutorial, you would need to install 

Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
5 and 
Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
6. I provided commands for both python library installation options: pip and poetry.

If you are using pip for installation [default]:

python -m pip install pytest
python -m pip install pytest-mock

If you are using poetry for installation:

poetry add pytest
poetry add pytest-mock

2. What is Unit Testing and Test Pyramid?

Unit Testing is a software testing method to test the smallest piece of code that can be isolated, i.e. a function [method].

The concept of Test Pyramid as shown below, is introduced by Mike Cohn in his book Succeeding with Agile. It defines the testing strategy with a great visualization, covering the three major types of tests: unit tests, integration tests and end-to-end tests, and the proportion of each out of the entire testing strategy.

According to the pyramid, when it is at the bottom it normally means more isolated and faster tests [in other words: cheaper]; while moving up the pyramid, it means more integrated and slower tests [in other words: expensive]. At the tip of the pyramid could means manual testing. Thus, we should cover code with more automated unit tests and less manual tests.

Test Pyramid

3. Why do we need Unit Testing?

People would tend to rush the requested feature delivering but ignoring the importance of writing test cases. The ignorance could be due to they see it as a waste of time and not seeing the damage it could cause. However, let me tell you the benefits that writing Unit tests will brings you:

  • Identify bugs easily and early: Unit test can help you verify what you want to develop.Without unit test, you would end up being caught by fixing random bugs when running entire application. And you may need to spend hours on placing breakpoints and tracing where does it come from.
  • Help you write better code: If you find it’s hard to write a test case for your own code, you probably would think about refactoring your code.
  • Test at low cost: Unit test is the fastest among all test cases as it removes all the dependencies.
  • Serve as documentation: People will understand what kind of input/output data type, format is when they look at your unit tests.

4. What makes a good unit test then?

Below are the six qualities that will make a good unit test. How we could achieve the first four qualities are covered in the sections below.

Independent

The testing is focusing on the function itself and NOT on all the dependencies, which include API call, DB connections, other functions from your application or third party libraries. All dependencies should be properly mocked.

Multiple assertions are allowed only if they are all testing one feature. When a test fails, it should pinpoint the location of the problem.

Tests should be isolated — not rely on each other. We shouldn’t make assumptions about the order of test execution.

Fast

A good unit test’s response time should be less than a second. This is also a good and straightforward way to evaluate its independency. Any unit test taking longer than that should be questioned in terms of its dependency. And maybe that’s no longer unit test but integration test.

Repeatable

The tests should produce the same output no matter when and how many time it runs.

Readable [Consistency]

The test case serves part of the function documentation and it’s frequently read during debugging processes. Thus, it’s important to be understood easily. Try to stick to one naming convention for naming test file, test case, sequence of assertion [

Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
7] and the way of writing mocks.

Automatic

The process of running the unit tests should be integrated into CI/CD tool such as Jenkins, so the code quality can be continuously ensured with every new change.

This is not explained in this article. Feel free to read more about Jenkins, GitHub Actions or any CI/CD tools.

Thorough [Coverage]

As mentioned in the Test Pyramid, we should get more unit tests as they are cheaper and faster. Coverage is the key metric to evaluate the degree of which source code has been tested. Any uncovered lines could result to a corner case bug one day with more expensive identification and resolving process.

Here is the formula to calculate code coverage, which is also called line coverage. This is mostly used.

Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100

If you ask me, what is the ideal code coverage that we should meet. There is no such thing that works for every product.

I would recommend to first reach 80% and make sure the coverage can be maintained with every single change, then continuously work on improving the code coverage towards 90%.
Efforts needed from 90% to 100% could be logarithmic, therefore usually target won’t go to as high as 100%.

5. How to write Unit Test with PyTest [Basics]?

Here are some basics to help you get started with PyTest.

Below is a typical folder structure for a Python project/application, where you have a 

Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
8 folder that is outside of your 
Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
9 folder.

.
├── docs                    # Documentation files [alternatively `doc`]
├── src                     # Source files [alternatively `lib` or `app`]
├── tests                    # Automated tests [alternatively `test`]
└── README.md
  1. Let’s assume we are testing the simple 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    0 function of 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    1 file under 
    Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
    9 folder
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
  2. Create a file named 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    3 inside the 
    Code Coverage = [Number of lines of code executed]/[Total Number of lines of code in a system component] * 100
    8 folder.The test file name should always start or end with 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    5. I prefer to keep the structure consistent as 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    6 where 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    7 where be py file name you are testing. You may read more about documentation on how test discovery works for pytest.
  3. Write the test case 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    8When writing the test cases, just define a function starting with 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    5 in the name. Similar as the file name, I prefer to keep it consistent as 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    0 where 
    .
    ├── docs                    # Documentation files [alternatively `doc`]
    ├── src                     # Source files [alternatively `lib` or `app`]
    ├── tests                    # Automated tests [alternatively `test`]
    └── README.md
    7 is the function you are testing. This provides a very clear understanding for others.And this follows Arrange-Act-Assert pattern to structure the test content. Though this example is very simple and straightforward and we could replace it with one line assertion, but let’s just use it for illustration purpose.
    • Arrange the input and targets: Does the test needs some special settings? Does it needs to prepare the database? Most of the time, we need to get the inputs ready and also mockup the dependencies before we proceed with the Act step.
    • Act on the targets: This usually refers to calling the function or method in the Unit Testing scenario.
    • Assert expected outcomes: We will receive certain responses from Act step. And Assert step verifies the goodness or badness of that response. This could be checking whether numbers/strings are correct, whether particular type of Exception was triggered or certain function was being triggered. Assertions will ultimately determine if the test passes or fails.

    For assertion statements [referring to

    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    2 part], generally, you could use any of logical conditions you would put similar as you write 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    3 statement.

    I didn’t always follow strictly Arrange-Act-Assert pattern to put each of them into a block. Sometimes I would combine Act and Assert just to save one line. But following this sequence always makes it looks neat and tidy.

    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​

    Note: We should consistently put output at the left handside and expected output at the right handside, meaning 

    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    4 not 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    5. This doesn’t make a difference in Terminal/CMD. But PyCharm will display it wrongly if we did it reversely.

  4. How to run PyTestYou could run PyTest test cases with any of the below commands.
    # run all tests
    python -m pytest tests
    
    # run single test file
    python -m pytest tests/test_calc.py
    
    # run single test case
    python -m pytest tests/test_calc.py::test_add​

    Note: If you are using poetry environment, you need to add 

    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    6 before any of the testing command.

  5. Try to think more situations where the function should be tested, so that you cover every aspect of it.

5.1 Parametrizing

Here, let me introduce the pytest parametrizing decorator, which checks whether multiple inputs lead to expected output. It’s a little bit similar as looping through each of the 3 input set [a+b], however, the testing result will let you know whether each of them has passed. But writing a loop of asserting each of them would stop at the middle once it failed.

This example tests when a=10, b=5, whether expected is 15; and similar for the other two cases.

import pytest
from src.calc import add


@pytest.mark.parametrize["a,b,expected",
							[[10, 5, 15],
							[-1, 1, 0],
							[-1, -1, -2]]]
def test_add[a, b, expected]:
	assert add[a, b] == expected

And here, we try to cover different scenarios of adding two positives, two negatives or one positive and one negative numbers. You may also add floating points test cases [which usually should be considered for testing division].

We have covered how to write a test case with pytest and parametrizing. Let’s look at another concept in PyTest, which is fixture.

5.2 Fixture

Fixture is a function with decorator that creates a resource and returns it. If you need the same test input for multiple test cases, you can use fixture to prepare the arrange step, just as the example below.

This reduces code duplications.

@pytest.fixture
def employee_obj_helper[]:
    """
    Test Employee Fixture
    """
    obj = Employee[first='Corey', last='Schafer', pay=50000]
    return obj

def test_employee_init[employee_obj]:
    employee_obj.first = 'Corey'
    employee_obj.last = 'Schafer'
    employee_obj.pay = 50000

def test_email[employee_obj]:
    assert employee_obj.email == 'Corey.Schafer@email.com'

def test_fullname[employee_obj]:
    assert employee_obj.fullname == 'Corey Schafer'

6. How to mock dependencies properly in various scenarios

We have learnt what makes a good Unit Test in the previous sections. And mocking is exactly the technique can help us writing independent, fast, repeatable test cases.

Mocking can be used to isolate and focus attention on the code under test rather than the behavior or state of external dependencies. Mocking replaces dependencies with carefully managed replacement objects that mimic the real ones’ behavior.

Let’s start with a simple mocking example.

Example 1:

We have a function sleep for couple of seconds. Let’s assume we have some other processing steps after the sleep.

def sleep_awhile[duration]:
    """sleep for couple of seconds"""
    time.sleep[duration]
    # some other processing steps    

And here is the test case.

poetry add pytest
poetry add pytest-mock
0
  1. We need to use 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    7 as part of test case inputs so we can call 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    8.
  2. We creates a mock object that replaces the 
    # src/calc.py
    
    def add[x, y]:
        """Add Function"""
        return x + y
    9 module with a fake object that do nothing, by specifying the target as “src.example.time.sleep”, meaning the 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    0 function inside 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    1. Here comes the rule of thumb for mocking: Mock where it is used, and not where it’s defined [source]. Here, 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    2 function doesn’t return anything so we just define 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    3 as 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    4.
  3. For this function 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    5 there is no output provided so we can’t verify that. So how do we know the test case is written properly?Thus, we check whether the mock object has been called with correct input using 
    import pytest
    from src.calc import add
    
    def test_add[]:
    	# Arrange
    	a = 2
    	b = 5
    	expected = 7
    
    	# Act
    	output = add[a, b]
    
    	# Assert
    	assert output == expected​
    6. And the test time should not be as long as 3 seconds.[should be

Bài mới nhất

Chủ Đề