Using the Mock in Python vs Java

Aaron Ginder
7 min readDec 10, 2024

--

Source: 7 Different Types of Software Testing

For those who have followed my previous articles, you know how much I enjoy testing code. In fact, if you’re looking to learn a new programming language or gain a deeper understanding of a package, I highly recommend writing unit tests. Testing forces you to explore client connection points, break the code you or someone else has written, and interrogate your work through scenario-based approaches with varied inputs.

In this article, I want to take a closer look at unit testing in both Java and Python, focusing on mocking techniques. I’ll walk you through examples in both languages, compare their approaches to testing, and ultimately evaluate which language provides a better or easier experience for mocking in real-world scenarios. Whether you’re a Java enthusiast, a Python fan, or just curious about how the two stack up, this comparison aims to highlight the strengths and weaknesses of each.

Before I continue, if you’re new to the concept of mocking, check out my article on 3 different ways to mock and object in Python:

https://aaronginder.medium.com/3-ways-to-implement-the-mock-during-python-unit-testing-f96cc9f5d589.

Comparison Between the Two Languages: A Unit Test Perspective

Lets start off comparing Python and Java. This will set the context for the rest of this article — to demonstrate the similarities and differences between the two languages.

Source: Aaron Ginder

To summarise the table, if you do not know either Python or Java, Python will be much easier to understand due to the simple, non-strict attribute of the programming language. There main differences between the two languages is where in the code the mock is implemented. In particular, Python allows for patch decorators above the function to create a mock object as an input to the function. On the other hand, Java is more verbose in code implementation; making it harder to read for those less familiar with Java. It would be wrong to say this makes Java less from ideal, there is much more control on mocking objects and the associated properties.

See below for a code demonstration on how to implement the mock in both languages!

Example: Write a Document to a MongoDB Client

Lets take a real-world, simple example. Suppose you have a Mongo Database (named financesDb) with a collection name of transactions. In your transactions collection will be your payment data associated to a transaction.

True unit tests will test each unit separately, both positive and negative cases, without any external dependencies. Considering this, you don’t need to connect to any document database (hooray, no installing MongoDB).

Given the scenario below, I have provided a simple function with the associated unit tests. If you keep reading, below the code snippets is an explanation of the code and how it works.

A Simple Scenario: Connecting to an existing database

You want to connect to your Mongo database. Rather than connecting, you will mock (imitate the object) and check if the appropriate methods have been called.

Python

# mongodb_connect.py
from pymongo import MongoClient

def connect_to_mongodb(uri: str, db_name: str):
"""Connect to MongoDB and return the database object."""
try:
client = MongoClient(uri)
db = client[db_name]
# Check if the connection is successful
client.admin.command('ping')
return db
except Exception as e:
raise ConnectionError(f"Failed to connect to MongoDB: {e}")

The connect_to_mongodb() function accepts two arguments: uri, which specifies the MongoDB connection string, and db_name, the name of the database to connect to. Inside the function, these arguments are used to initialize a MongoClient instance, which establishes a connection to the MongoDB server. To validate the connection, the function sends a ping command to the admin database. If the ping succeeds, the function returns the specified database object. Any exceptions that occur during the connection or validation process are caught in the except block, where a ConnectionError is raised with a descriptive error message. This ensures that failures are appropriately handled and communicated without abruptly crashing the program. For unit testing, this connection can be mocked to avoid connecting to an actual database.

# test_mongodb_connect.py
import unittest
from unittest.mock import patch, MagicMock
from your_module import connect_to_mongodb

class TestMongoDBConnection(unittest.TestCase):

@patch('mongodb_connect.MongoClient')
def test_connect_to_mongodb_success(self, mock_mongo_client):
"""Test successful MongoDB connection."""
# Mock the MongoClient and its behavior
mock_client_instance = MagicMock()
mock_mongo_client.return_value = mock_client_instance
mock_client_instance.admin.command.return_value = {"ok": 1}

# Call the function
db_name = "financesDb"
uri = "mongodb://localhost:27017"
db = connect_to_mongodb(uri, db_name)

# Assertions
mock_mongo_client.assert_called_with(uri) # Verify MongoClient was called with correct URI
mock_client_instance.admin.command.assert_called_with('ping') # Verify ping command was called
self.assertEqual(db, mock_client_instance[db_name]) # Verify returned db instance

@patch('mongodb_connect.MongoClient')
def test_connect_to_mongodb_failure(self, mock_mongo_client):
"""Test MongoDB connection failure."""
# Simulate a connection failure
mock_client_instance = MagicMock()
mock_mongo_client.return_value = mock_client_instance
mock_client_instance.admin.command.side_effect = Exception("Connection failed")

# Call the function and verify it raises ConnectionError
with self.assertRaises(ConnectionError) as context:
connect_to_mongodb("mongodb://localhost:27017", "financesDb")

self.assertIn("Failed to connect to MongoDB", str(context.exception))


if __name__ == '__main__':
unittest.main()

The TestMongoDBConnection class inherits from Python’s unittest.TestCase to leverage the unit testing framework. Each test case is decorated with the @patch method to mock the MongoClient import path used in the connect_to_mongodb function. This ensures that no actual database connection is made, and the properties of the MongoClient are simulated through the mock object.

Two test cases are implemented: one for a successful connection and one for a failed connection. In the success case, the mock MongoClient is configured to simulate a successful ping command and the existence of the financesDb database. Assertions verify that the mock MongoClient is called with the correct URI, the database ping succeeds, and the returned database object matches the expected mock database.

In the failure case, the mock MongoClient is configured with a side effect that raises an exception when the ping command is called, simulating a failed connection. The test then runs the connect_to_mongodb function within a context manager that captures the raised exception, allowing assertions to verify that the exception message matches the expected error. This approach ensures robust validation of both successful and failed connection scenarios.

Java

// MongoDBUtils.java
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoDatabase;

public class MongoDBUtils {

/**
* Connect to MongoDB and return the database object.
*
* @param uri MongoDB connection string
* @param dbName Name of the database
* @return MongoDatabase instance
* @throws RuntimeException if connection fails
*/
public MongoDatabase connectToMongoDB(String uri, String dbName) {
try {
MongoClient client = new MongoClient(new MongoClientURI(uri));
MongoDatabase database = client.getDatabase(dbName);

// Test the connection
client.getDatabase("admin").runCommand(new org.bson.Document("ping", 1));
return database;
} catch (Exception e) {
throw new RuntimeException("Failed to connect to MongoDB: " + e.getMessage(), e);
}
}

/**
* Main method to test or execute the connectToMongoDB functionality.
*/
public static void main(String[] args) {
MongoDBUtils mongoDBUtils = new MongoDBUtils();

// MongoDB URI and database name
String uri = "mongodb://localhost:27017";
String dbName = "financesdb";

try {
// Call the connectToMongoDB method
MongoDatabase database = mongoDBUtils.connectToMongoDB(uri, dbName);
System.out.println("Successfully connected to the database: " + dbName);
} catch (RuntimeException e) {
// Handle connection errors
System.err.println("Error: " + e.getMessage());
}
}
}

Similar to the Python code, the MongoDBUtils class takes two parameters: uri and dbName. The main method executes the connectToMongoDB method passing the parameters above. This method creates a MongoClient (again, we want to mock this), gets the database name and then pings the database to ensure connection has been established.

// MongoDBUtilsTest.java
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.MongoException;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class MongoDBUtilsTest {

@Test
void testConnectToMongoDBSuccess() {
// Mock MongoClient and related behavior
MongoClient mockClient = mock(MongoClient.class);
MongoDatabase mockDatabase = mock(MongoDatabase.class);

when(mockClient.getDatabase("admin")).thenReturn(mockDatabase);
when(mockDatabase.runCommand(new Document("ping", 1))).thenReturn(new Document("ok", 1));

// Test the method
MongoDBUtils mongoDBUtils = Mockito.spy(new MongoDBUtils());

// Use doReturn to return the mocked client instead of creating a real one
doReturn(mockClient).when(mongoDBUtils).createMongoClient(any(MongoClientURI.class));

MongoDatabase result = mongoDBUtils.connectToMongoDB("mongodb://localhost:27017", "testDb");

// Verify behavior
assertNotNull(result);
verify(mockClient).getDatabase("admin");
verify(mockDatabase).runCommand(new Document("ping", 1));
}

@Test
void testConnectToMongoDBFailure() {
// Mock MongoClient and simulate an exception during connection
MongoClient mockClient = mock(MongoClient.class);
MongoDatabase mockDatabase = mock(MongoDatabase.class);

when(mockClient.getDatabase("admin")).thenReturn(mockDatabase);
when(mockDatabase.runCommand(new Document("ping", 1))).thenThrow(new MongoException("Connection failed"));

// Test the method
MongoDBUtils mongoDBUtils = Mockito.spy(new MongoDBUtils());
doReturn(mockClient).when(mongoDBUtils).createMongoClient(any(MongoClientURI.class));

Exception exception = assertThrows(RuntimeException.class, () -> {
mongoDBUtils.connectToMongoDB("mongodb://localhost:27017", "testDb");
});

// Verify exception message
assertTrue(exception.getMessage().contains("Failed to connect to MongoDB"));
}
}

The flow of the code is similar to the Python example described earlier, but the primary difference lies in how mocking is handled. Here, I use Mockito to mock Java objects like MongoClient and MongoDatabase, whereas in Python, I used unittest.mock. Java uses JUnit as the testing framework, and test methods are annotated with @Test to signal they should be executed as part of the test suite, compared to Python’s @patch decorator for dynamic patching. Instead of decorating a method, Java explicitly configures the behavior of mocks, allowing fine-grained control. In the success test, a mock client and database are created, and the return value for getDatabase("admin") is set to the mock database. A mock response for runCommand with {"ok": 1} simulates a successful connection. The failure test simulates an exception during runCommand to validate error handling. While Python’s @patch simplifies patching inline, Java’s approach explicitly uses doReturn and verify for mock behavior and interaction validation, providing robust but more verbose testing.

Closing Remarks

Mocking in Python and Java achieves the same goal of simulating objects and behaviours for testing purposes, but the approaches differ significantly in implementation and style. Both Python’s unittest.mock and Java's Mockito provide tools to create mock objects, configure their behavior, and verify interactions. In Python, the use of decorators like @patch allows for concise and dynamic substitution of objects, making it quick and readable for simple cases. Java, on the other hand, relies on explicitly creating mocks and configuring behavior using methods like when, doReturn, and verify, which offers more explicit control but can be more verbose. While Python’s dynamic and flexible nature allows for streamlined mocking, Java's approach emphasizes type safety and explicitness. Ultimately, both frameworks are robust, and the choice of one over the other depends on the language and the specific requirements of the test environment.

--

--

Aaron Ginder
Aaron Ginder

Written by Aaron Ginder

An enthusiastic technologist looking to share my passion for cloud computing, programming and software engineering.

No responses yet