Skip to content

Automatisch Testen

Bart Snijder edited this page Jul 13, 2025 · 11 revisions

'Automated Testing' is een techniek om software te testen (te valideren) op correcte functionaliteit, zie hier en dit artikel voor uitleg hierover. Deze tak van sport is enorm gegroeid dankzij 'Agile' en 'DevOps' methodieken, wat je ook weer terug kun zien in 'Continuous Delivery' (CD) & 'Continious Integration' (CI). Doel: het software(test)proces sneller maken, door het testen zoveel mogelijk te automatiseren. Zodra ontwikkelaars nieuwe functionaliteit in de software hebben geschreven en in de repository hebben ingecheckt, controleert CI de nieuwe code op fouten en gevaren en CD controleert of de software wel goed werkt, automatisch! Zie: bron.

Unittest: de ingebouwde Python optie

Na een zeer kort onderzoek op Internet trek als voorzichtig conclusie: het eerste om eens uit te proberen is de ingebouwde module unittest. Handig, installeren is niet nodig. bron. Een ander, mogelijk groot voordeel van unittest is het bieden van 'mocking'. Een 'mock' of een 'mock-up' is hulpsoftware dat fysieke interfaces als het ware 'faket', zodat je kan testen, zonder de normaal benodigde hardware of netwerkconnecties. Daarnaast blijkt er een interessante, overkoepelende aanstuursoftware te zijn voor unittest: coverage.py. Zie link

#Niet mokken maar mocken De unittest module heeft interessante submodules: Mock. Een goed blog over het gebruik van Mock kun je hier lezen.

Een mock-up voor VISA

Pyvisa-sim, de simulator van Python uitvoering van VISA, werkt helaas niet echt lekker: je moet behoorlijk editen in de 'happy flow' van de code om pyvisa-sim werkend te krijgen. Dat past niet in het denken over gestructureerd software schrijven. Gelukkig blijkt er, in aansluiting over bovenstaande opmerkingen over mock-ups een pyvisa mock-up implementatie te bestaan:

afbeelding

Op de github pagina van deze mock staat een korte uitleg. Veel belovend: in plaats van een obscure yml file kun je heldere code scfhrijven, zoals hieronder. Dit lijkt beter dan pyvisa. De vraag is of dit wel goed werkt.

afbeelding

Zie link

OverzichtAutomatisch Testen met Python

Hieronder volgt een korte, niet specifiek gestructureerd en absoluut niet uitputtend, overzicht van aantal opties:

Testen in Visual Studio Code

Zie hier voor een beschrijving.

Geeks for Geeks

link:

  1. LambdaTest. AI platform, maakt mogelijk om andere testplatformen te integreren.
  2. Unittest i.c.m. Nose2

Een goed artikel over Mocking in Python

Een hele goede uitleg betreffende Mocking in Python vind je hier Ook een aardige: een soort van cheatsheet, korte voorbeeldjes waar van alles en nog wat misgaat. zie hier

The Art of Mocking in Python: A Comprehensive Guide Moraneus

Moraneus ·

Follow 10 min read · May 21, 2024

When writing software, testing is crucial to ensure that your code behaves as expected. However, testing can sometimes be challenging, especially when your code depends on external systems like databases, APIs, or even hardware devices. This is where mocking comes into play. In this article, we’ll dive deep into the concept of mocking, explore how to use part of the unittest.mock module in Python, and provide useful examples to learn and start with. We'll also discuss alternatives to mocking and when you might want to use them. What is Mocking?

Mocking is a technique used in unit testing to simulate the behavior of real objects. By creating mock objects, you can test your code in isolation, without relying on external dependencies. This makes your tests faster, more reliable, and easier to maintain.

Why Use Mocking?

  • Isolation: Test individual components without dependencies.
  • Speed: Avoid slow operations like network calls or database queries.
  • Control: Simulate specific scenarios and edge cases.
  • Simplicity: Simplify complex test setups.

Getting Started with unittest.mock

The unittest.mock module, included in Python's standard library, provides a powerful and flexible way to create mock objects. Let's start with a basic. example of mocking an API Call.

Suppose we have a simple function that fetches data from an API: """ import requests from typing import Dict

def get_data(url: str) -> Dict[str, str]:` response = requests.get(url) return response.json() """ To test this function without making an actual HTTP request, we can use mocking:

from unittest.mock import patch import unittest from typing import Dict

class TestGetData(unittest.TestCase): @patch('requests.get') def test_get_data(self, mock_get: patch) -> None: mock_response = mock_get.return_value # Create a mock response object mock_response.json.return_value = {'key': 'value'} # Define the return value of the mock response's json method

    url = 'http://example.com/api'
    result = get_data(url)  # Call the function with the mocked response
    
    self.assertEqual(result, {'key': 'value'})  # Assert the returned value is as expected
    mock_get.assert_called_once_with(url)  # Ensure the get method was called once with the correct URL

if name == 'main': unittest.main()

Explanation

The @patch decorator is a powerful tool for replacing parts of your system under test with mock objects.

Here's a breakdown of how it works:

Decorator Function: @patch('requests.get') tells the test runner to replace requests.get with a mock object.
Mock Object Injection: The mock object is passed as an argument to the test function (mock_get in this case).
Return Value: The mock object can be configured to return specific values or raise exceptions using its methods (e.g., mock_response.json.return_value).

By using the @patch decorator, you don't need to manually start and stop the patching, making your test code cleaner and easier to understand. Mocking File Operations

Consider a function that reads data from a file:

def read_file(filepath: str) -> str: with open(filepath, 'r') as file: return file.read()

This function takes a file path as an argument, opens the file in read mode, reads its contents, and returns them as a string. To test this function without creating an actual file on the filesystem, we can use the unittest.mock module to mock the built-in open function.

Here’s the test code:

from unittest.mock import patch import unittest

class TestReadFile(unittest.TestCase): @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data='mock data') def test_read_file(self, mock_open: patch) -> None: result = read_file('dummy_path') # Call the function with the mocked open function

    self.assertEqual(result, 'mock data')  # Assert the returned value is as expected
    mock_open.assert_called_once_with('dummy_path', 'r')  # Ensure the open method was called once with the correct filepath and mode

if name == 'main': unittest.main()

Explanation

Decorator Function: The @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data='mock data') decorator tells the test runner to replace the built-in open function with a mock object.

'builtins.open': This argument specifies the target to be replaced with a mock object. 'builtins.open' refers to the built-in open function in Python. If we were patching a function or method from a module, the target would be specified as 'module_name.function_name'.
new_callable=unittest.mock.mock_open: This argument specifies the type of mock object to use. unittest.mock.mock_open is a helper function that creates a mock to replace open. This mock object can be used to simulate file operations.
read_data='mock data': This argument specifies the data that should be returned when the mock file is read. By setting read_data='mock data', we configure the mock to return 'mock data' when its read method is called.

Mock Object Injection: The mock object is automatically passed as an argument to the test function (mock_open in this case).
Calling the Function: result = read_file('dummy_path') calls the read_file function with 'dummy_path' as the argument. Since the open function has been mocked, no actual file is opened. Instead, the mock object created by mock_open is used. The read method of this mock object returns 'mock data' as configured.
Asserting the Return Value: self.assertEqual(result, 'mock data') checks that the result of calling read_file('dummy_path') is 'mock data'. This verifies that the function behaves correctly when open is mocked.
Verifying the Mock Call: mock_open.assert_called_once_with('dummy_path', 'r') checks that the open function (replaced by the mock object) was called exactly once with the arguments 'dummy_path' and 'r'. This verifies that the function attempts to open the correct file in the correct mode.

By mocking the open function, we can simulate file operations without creating actual files on the filesystem, ensuring consistent and predictable test results. Mocking Time

Sometimes, you need to mock time-related functions in your tests to control and predict the passage of time, ensuring consistent and reliable test results.

Let’s start by defining the function we want to test:

import time

def get_timestamp() -> float: return time.time()

This function simply returns the current timestamp by calling time.time().

Here’s the test code for the get_timestamp function, which mocks the time.time() function:

from unittest.mock import patch import unittest

class TestGetTimestamp(unittest.TestCase): @patch('time.time', return_value=1625097600.0) # Mocking time.time to return a fixed timestamp def test_get_timestamp(self, mock_time) -> None: result = get_timestamp() # Call the function with the mocked time function

    self.assertEqual(result, 1625097600.0)  # Assert the returned value is as expected
    mock_time.assert_called_once()  # Ensure the time method was called once

if name == 'main': unittest.main()

Explanation

The @patch decorator is used to replace the time.time function with a mock object.

Here's a detailed breakdown:

Decorator Function: The @patch('time.time', return_value=1625097600.0) decorator tells the test runner to replace the time.time function with a mock object. The target 'time.time' specifies that we are patching the time function from the time module. By using return_value=1625097600.0, we configure the mock to return 1625097600.0 whenever time.time is called. This value corresponds to a specific fixed point in time (specifically, July 1, 2021, at 00:00:00 UTC).
Mock Object Injection: The mock object is automatically passed as an argument to the test function (mock_time in this case). This allows us to interact with and configure the mock within the test.
Configuring the Mock Return Value: The mock object is configured to return a specific value using return_value. In this example, mock_time.return_value is set to 1625097600.0, which represents a fixed timestamp. Whenever the get_timestamp function calls time.time(), it receives this fixed timestamp instead of the current time.
Calling the Function: result = get_timestamp() calls the get_timestamp function. Since the time.time function has been mocked, it returns 1625097600.0 instead of the current time.
Asserting the Return Value: self.assertEqual(result, 1625097600.0) checks that the result of calling get_timestamp() is `1625097600.
Verifying the Mock Call: mock_time.assert_called_once() ensures that the time.time function (replaced by the mock object) was called exactly once. This verifies that the function attempts to get the current time.

By mocking the time.time function, we can control the timestamp returned to the get_timestamp function, ensuring consistent and predictable test results. Mocking Classes and Methods

Sometimes, you need to mock entire classes or specific methods within a class. Consider a class Database that has methods to connect to a database and fetch data:

from typing import Dict

class Database: def connect(self) -> None: pass

def fetch_data(self) -> Dict[str, str]:
    return {'data': 'real data'}

We also have a function process_data that uses this Database class:

def process_data() -> Dict[str, str]: db = Database() db.connect() return db.fetch_data()

To test the process_data function without actually connecting to a real database, we can use mocking.

Here’s the test code for the process_data function:

from unittest.mock import patch, MagicMock import unittest

class TestProcessData(unittest.TestCase): @patch('main.Database') def test_process_data(self, MockDatabase: MagicMock) -> None: mock_db = MockDatabase.return_value # Create an instance of the mock Database class mock_db.fetch_data.return_value = {'data': 'mock data'} # Define the return value of the fetch_data method

    result = process_data()  # Call the function with the mocked Database
    
    self.assertEqual(result, {'data': 'mock data'})  # Assert the returned value is as expected
    mock_db.connect.assert_called_once()  # Ensure the connect method was called once

if name == 'main': unittest.main()

Explanation

Decorator Function: The @patch('__main__.Database') decorator tells the test runner to replace the Database class in the __main__ module with a mock object. This allows us to control and verify the behavior of the Database class during the test.
Mock Object Injection: The mock class is automatically passed as an argument to the test function (MockDatabase in this case). This allows us to interact with and configure the mock class within the test.
Creating a Mock Instance: mock_db = MockDatabase.return_value creates an instance of the mock Database class. This mock instance will be used to replace the actual Database class in the process_data function.
Configuring the Mock Return Value: mock_db.fetch_data.return_value = {'data': 'mock data'} sets the return value of the fetch_data method of the mock Database instance to {'data': 'mock data'}. This means that when fetch_data is called on the mock Database instance, it will return {'data': 'mock data'} instead of the real data.
Calling the Function: result = process_data() calls the process_data function. Since the Database class has been mocked, the function uses the mock Database instance instead of a real one.
Asserting the Return Value: self.assertEqual(result, {'data': 'mock data'}) checks that the result of calling process_data is {'data': 'mock data'}. This verifies that the function behaves correctly when the Database class is mocked.
Verifying the Mock Call: mock_db.connect.assert_called_once() checks that the connect method of the mock Database instance was called exactly once. This verifies that the function attempts to connect to the database.

Mocking Async Functions

With the rise of asynchronous programming, you might need to mock async functions. Here’s how you can do it using the unittest.mock module in Python:

Consider an asynchronous function that fetches data from an API:

import aiohttp import asyncio from typing import Dict

async def fetch_data(url: str) -> Dict[str, str]: async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.json()

To test this function without making an actual HTTP request, we can use mocking:

from unittest.mock import AsyncMock, patch import unittest import asyncio

class TestFetchData(unittest.TestCase): @patch('aiohttp.ClientSession') def test_fetch_data(self, MockClientSession: AsyncMock) -> None: # Create an instance of the mock session mock_session = MockClientSession.return_value mock_session.aenter.return_value = mock_session

    # Configure the mock response
    mock_response = AsyncMock()
    mock_response.json.return_value = {'key': 'value'}
    mock_session.get.return_value.__aenter__.return_value = mock_response
    
    # Run the async function within the event loop
    result = asyncio.run(fetch_data('http://example.com/api'))
    
    # Assertions
    self.assertEqual(result, {'key': 'value'})
    mock_session.get.assert_called_once_with('http://example.com/api')

if name == 'main': unittest.main()

Explanation

Patching the ClientSession Class

The @patch('aiohttp.ClientSession') decorator tells the test runner to replace the aiohttp.ClientSession class with a mock object. This allows us to control the behavior of the entire session. Here’s how it works:

Decorator Function: @patch('aiohttp.ClientSession') tells the test runner to replace the ClientSession class from the aiohttp module with a mock object.
Mock Object Injection: The mock class is automatically passed as an argument to the test function (MockClientSession in this case). This allows us to interact with and configure the mock within the test.

Creating and Configuring the Mock Session

mock_session = MockClientSession.return_value creates an instance of the mock session. This mock session will be used in place of a real ClientSession.
mock_session.__aenter__.return_value = mock_session configures the mock session to return itself when used as an asynchronous context manager. This is necessary because the ClientSession is used within a with statement in the fetch_data function.

Configuring the Mock Response

mock_response = AsyncMock() creates an AsyncMock instance for the response. This mock object will simulate the behavior of the HTTP response returned by the get method.
mock_response.json.return_value = {'key': 'value'} configures the json method of the mock response to return {'key': 'value'} when awaited.
mock_session.get.return_value.__aenter__.return_value = mock_response sets up the mock get method to return the mock response when awaited. This means that when session.get(url) is called, it returns the mock_response object.

Running the Asynchronous Function

result = asyncio.run(fetch_data('http://example.com/api')) runs the fetch_data function within the event loop and gets the result. The asyncio.run function is used to run the asynchronous function in a synchronous context.

Assertions

self.assertEqual(result, {'key': 'value'}) checks that the result of calling fetch_data is {'key': 'value'}. This verifies that the function correctly processes the mock response.
mock_session.get.assert_called_once_with('http://example.com/api') verifies that the get method was called with the correct URL. This ensures that the fetch_data function made the expected HTTP request.

Alternatives to Mocking

While mocking is a powerful tool, it’s not always the best choice. Here are some alternatives:

Fakes: Create simplified versions of real objects that mimic their behavior. For example, a fake database might store data in memory rather than on disk.
Stubs: Provide hard-coded responses for specific method calls. This is useful when you need consistent responses for your tests.
Spies: Track interactions with real objects without modifying their behavior. Spies can be used to verify that methods are called with the correct arguments.
Integration Tests: Test your code with real dependencies in a controlled environment. Integration tests are essential for ensuring that all components work together correctly.

Conclusion

Mocking is an essential technique for writing effective unit tests in Python. By using the unittest.mock module, you can create mock objects to isolate your tests, making them faster and more reliable. We've covered the basics of mocking, with some useful techniques, and alternatives to help you become proficient in writing tests for your Python code.

Remember, the key to successful testing is to choose the right tool for the job. Whether you use mocks, fakes, stubs, or spies, the goal is to ensure that your code works correctly in all scenarios. Happy testing! Your Support Means a Lot! 🙌

If you enjoyed this article and found it valuable, please consider giving it a clap to show your support. Feel free to explore my other articles, where I cover a wide range of topics related to Python programming and others. By following me, you’ll stay updated on my latest content and insights. I look forward to sharing more knowledge and connecting with you through future articles. Until then, keep coding, keep learning, and most importantly, enjoy the journey! Reference unittest.mock - mock object library Source code: Lib/unittest/mock.py unittest.mock is a library for testing in Python. It allows you to replace parts of…

docs.python.org Python Python Programming Mockup Testing Unittest

Clone this wiki locally