Skip to content

Commit fee884f

Browse files
committed
Java: port java/mocking-all-non-private-methods-means-unit-test-is-too-big query
1 parent dfe4401 commit fee884f

27 files changed

+1325
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# J-T-001: Mocking all non-private methods of a class may indicate the unit test is testing too much
2+
3+
Mocking too many non-private methods of a class may indicate that the test is too complicated, possibly because it is trying to test multiple things at once.
4+
5+
## Overview
6+
7+
Mocking methods of a class is necessary for a unit test to run without overhead caused by expensive I/O operations necessary to compute their values. However, if a unit test ends up mocking all of them, it is likely a signal that the scope of the unit test is reaching beyond a single unit of functionality.
8+
9+
## Recommendation
10+
11+
It is best to contain the scope of a single unit test to a single unit of functionality. For example, a unit test may aim to test a series of data-transforming functions that depends on an ORM class. Even though the functions may be semantically related with one another, it is better to create a unit test for each function.
12+
13+
## Example
14+
15+
The following example mocks all methods of an ORM class named `EmployeeRecord`, and tests four functions against them. Since the scope of the unit test harbors all four of them, all of the methods provided by the class are mocked.
16+
17+
```java
18+
public class EmployeeRecord {
19+
public int add(Employee employee) { ... }
20+
21+
public Employee get(String name) { ... }
22+
23+
public int update(Employee employee, String newName) { ... }
24+
25+
public int delete(Employee employee) { ... }
26+
}
27+
28+
public class TestORM {
29+
@Test
30+
public void nonCompliant() {
31+
Employee sampleEmployee = new Employee("John Doe");
32+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: Mocked class has all of its public methods used in the test
33+
when(employeeRecordMock.add(Employee.class)).thenReturn(0); // Mocked EmployeeRecord.add
34+
when(employeeRecordMock.get(String.class)).thenReturn(sampleEmployee); // Mocked EmployeeRecord.get
35+
when(employeeRecordMock.update(Employee.class, String.class)).thenReturn(0); // Mocked EmployeeRecord.update
36+
when(employeeRecordMock.delete(Employee.class)).thenReturn(0); // Mocked EmployeeRecord.delete
37+
}
38+
39+
@Test
40+
public void compliant1() {
41+
Employee sampleEmployee = new Employee("John Doe");
42+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
43+
when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
44+
when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
45+
}
46+
47+
}
48+
```
49+
50+
## Implementation Notes
51+
52+
JUnit provides two different ways of mocking a method call: `when(mockedObject.methodToMock(...)).thenReturn(...)` and `doReturn(...).when(mockedObject).methodToMock(...)`. Both forms are taken into account by the query.
53+
54+
## References
55+
56+
- Baeldung: [Best Practices for Unit Testing in Java](https://www.baeldung.com/java-unit-testing-best-practices).
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @id java/mocking-all-non-private-methods-means-unit-test-is-too-big
3+
* @name J-T-001: Mocking all non-private methods of a class may indicate the unit test is testing too much
4+
* @description Mocking all non-private methods provided by a class might indicate the unit test
5+
* aims to test too many things.
6+
* @kind problem
7+
* @precision high
8+
* @problem.severity recommendation
9+
* @tags maintainability
10+
* readability
11+
*/
12+
13+
import java
14+
15+
class MockitoMockCall extends MethodCall {
16+
MockitoMockCall() { this.getMethod().hasQualifiedName("org.mockito", "Mockito", "mock") }
17+
18+
/**
19+
* Gets the type that this call intends to mock. e.g.
20+
* ``` java
21+
* EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class);
22+
* ```
23+
* This predicate gets the class `EmployeeRecord` in the above example.
24+
*/
25+
Type getMockedType() { result = this.getAnArgument().(TypeLiteral).getReferencedType() }
26+
}
27+
28+
/**
29+
* A method call that mocks a target method in a JUnit test. e.g.
30+
* ``` java
31+
* EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class);
32+
* when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
33+
* doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
34+
* ```
35+
* This class captures the call to `add` which mocks the equivalent method of the class `EmployeeRecord`.
36+
*/
37+
class MockitoMockingMethodCall extends MethodCall {
38+
MockitoMockCall mockCall;
39+
40+
MockitoMockingMethodCall() {
41+
/* 1. The qualifier originates from the mock call. */
42+
this.getQualifier().getControlFlowNode().getAPredecessor+() = mockCall.getControlFlowNode() and
43+
/* 2. The mocked method can be found in the class being mocked with the mock call. */
44+
mockCall.getMockedType().(ClassOrInterface).getAMethod() = this.getMethod()
45+
}
46+
47+
/**
48+
* Gets the call to Mockito's `mock` from which the qualifier, the mocked object, originates.
49+
*/
50+
MockitoMockCall getMockitoMockCall() { result = mockCall }
51+
}
52+
53+
/*
54+
* The following from-which-select embodies this pseudocode:
55+
* - Find a JUnit4TestMethod which:
56+
* - for a class that it mocks with a call to `mock`,
57+
* - for all methods that the class has, there is a method that this test method mocks.
58+
*/
59+
60+
from JUnit4TestMethod testMethod, ClassOrInterface mockedClassOrInterface
61+
where
62+
exists(MockitoMockCall mockCall |
63+
mockCall.getParent+().(Stmt) = testMethod.getBody().getAStmt() and
64+
mockedClassOrInterface = mockCall.getMockedType() and
65+
forex(Method method | method = mockedClassOrInterface.getAMethod() and method.isPublic() |
66+
exists(MockitoMockingMethodCall mockedMethod |
67+
mockedMethod.getMockitoMockCall() = mockCall and
68+
mockedMethod.getMethod() = method
69+
)
70+
)
71+
)
72+
select testMethod, "This test method mocks all public methods of a $@.", mockedClassOrInterface,
73+
"class or an interface"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Underlying data type of the ORM class and functions.
3+
*/
4+
public class Employee {
5+
Employee(String name) {
6+
this.name = name;
7+
}
8+
9+
String name;
10+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Sample ORM class for the type `Employee`.
3+
*/
4+
public class EmployeeRecord {
5+
public int add(Employee employee) {
6+
return 1;
7+
}
8+
9+
public Employee get(String name) {
10+
return new Employee("Sample");
11+
}
12+
13+
public int update(Employee employee, String newName) {
14+
return 1;
15+
}
16+
17+
public int delete(Employee employee) {
18+
return 1;
19+
}
20+
21+
private void f() { }
22+
23+
private void g() { }
24+
25+
private void h() { }
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| TestORM.java:34:15:34:27 | nonCompliant1 | This test method mocks all public methods of a $@. | EmployeeRecord.java:4:14:4:27 | EmployeeRecord | class or an interface |
2+
| TestORM.java:47:15:47:27 | nonCompliant2 | This test method mocks all public methods of a $@. | EmployeeRecord.java:4:14:4:27 | EmployeeRecord | class or an interface |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Likely Bugs/Frameworks/JUnit/MockingAllNonPrivateMethodsMeansUnitTestIsTooBig.ql
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import org.junit.Test;
2+
import static org.mockito.Mockito.mock;
3+
import static org.mockito.Mockito.when;
4+
import static org.mockito.Mockito.doReturn;
5+
6+
public class TestORM {
7+
/**
8+
* Test of form `when(mockedObject.methodToBeMocked()).thenReturn(someVal)`.
9+
*/
10+
@Test
11+
public void compliant1() {
12+
Employee sampleEmployee = new Employee("John Doe");
13+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
14+
when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
15+
when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
16+
}
17+
18+
/**
19+
* Test of form `doReturn(someVal).when(mockedObject).methodToBeMocked()`.
20+
*/
21+
@Test
22+
public void compliant2() {
23+
Employee sampleEmployee = new Employee("John Doe");
24+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // COMPLIANT: Only some of the public methods belonging to the mocked object are used
25+
doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
26+
doReturn(0).when(employeeRecordMock).get("John Doe"); // Mocked EmployeeRecord.get
27+
doReturn(0).when(employeeRecordMock).delete(sampleEmployee); // Mocked EmployeeRecord.delete
28+
}
29+
30+
/**
31+
* Test of form `when(mockedObject.methodToBeMocked()).thenReturn(someVal)`.
32+
*/
33+
@Test
34+
public void nonCompliant1() {
35+
Employee sampleEmployee = new Employee("John Doe");
36+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: All public methods of the mocked object are used
37+
when(employeeRecordMock.add(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.add
38+
when(employeeRecordMock.get("John Doe")).thenReturn(sampleEmployee); // Mocked EmployeeRecord.get
39+
when(employeeRecordMock.update(sampleEmployee, "Jane Doe")).thenReturn(0); // Mocked EmployeeRecord.update
40+
when(employeeRecordMock.delete(sampleEmployee)).thenReturn(0); // Mocked EmployeeRecord.delete
41+
}
42+
43+
/**
44+
* Test of form `doReturn(someVal).when(mockedObject).methodToBeMocked()`.
45+
*/
46+
@Test
47+
public void nonCompliant2() {
48+
Employee sampleEmployee = new Employee("John Doe");
49+
EmployeeRecord employeeRecordMock = mock(EmployeeRecord.class); // NON_COMPLIANT: All public methods of the mocked object are used
50+
doReturn(0).when(employeeRecordMock).add(sampleEmployee); // Mocked EmployeeRecord.add
51+
doReturn(0).when(employeeRecordMock).get("John Doe"); // Mocked EmployeeRecord.get
52+
doReturn(0).when(employeeRecordMock).update(sampleEmployee, "Jane Doe"); // Mocked EmployeeRecord.update
53+
doReturn(0).when(employeeRecordMock).delete(sampleEmployee); // Mocked EmployeeRecord.delete
54+
}
55+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../stubs/junit-4.13:${testdir}/../../stubs/mockito-5.14

0 commit comments

Comments
 (0)