解决 JUnit 错误:测试类应具有恰好一个公共无参数构造函数
最后更新:2025年12月25日
1. 概述
在用 Java 编写单元测试时,我们可能会遇到 JUnit 错误:测试类应具有恰好一个公共无参数构造函数。这通常发生在 JUnit 4 中,当我们的测试类定义了带有参数的构造函数时。
在本教程中,我们将重点介绍如何解决所讨论的错误。在接下来的部分中,我们将展示如何通过使用参数化测试、升级到 JUnit 5 或避免参数化构造函数来修复该错误。
2. 理解错误
在运行任何测试方法之前,JUnit 通常会创建测试类的新实例,以便每个测试独立运行,并且不与其他测试共享状态。具体来说,在 JUnit 4 中,这个过程依赖于找到一个公共无参数构造函数。
通常,如果一个类没有定义任何构造函数,Java 会自动提供一个公共无参数构造函数。这个隐式构造函数的目的是帮助 JUnit 4 使用反射实例化测试类。
然而,一旦我们定义了自己的构造函数,特别是接受参数的构造函数,Java 就会停止生成默认构造函数。这确保了在创建对象时不会对要调用哪个构造函数产生混淆。当发生这种情况时,JUnit 4 无法再调用 new TestClass(),因为构造函数不再存在,从而抛出所讨论的错误
public class ResolvingJUnitConstructorErrorUnitTest {
private int input;
// Constructor with a parameter (causes the error)
public ResolvingJUnitConstructorErrorUnitTest(int input) {
...
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
...
}
}
当我们考虑上面的例子时,JUnit 4 无法运行测试,因为它不知道为我们的构造函数 ResolvingJUnitConstructorErrorUnitTest 提供什么值,即int参数。由于我们不使用无参数构造函数,JUnit 4 无法在执行测试方法之前自动创建实例。
JUnit 依赖无参数构造函数是故意的。当我们始终从一个新的、无参数的实例开始时,JUnit 确保每个测试独立运行,从而消除了共享状态的风险。例如,它可以防止一个测试修改另一个测试稍后依赖的字段。有了这种隔离,测试可以保证可预测和可重复的结果。
如果我们在示例中引入了参数化构造函数,我们将破坏受控的生命周期。因此,JUnit 无法自动构造测试类,并抛出“测试类应具有恰好一个公共无参数构造函数”错误。
3. 重现错误
在本节中,我们可以使用一个最小的 Maven 项目来重现该错误。之后,让我们在项目的 pom.xml 文件中添加 JUnit 4 作为依赖项
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
上面的代码块将 JUnit 添加为依赖项。
现在让我们创建主类,ResolvingJUnitConstructorError
public class ResolvingJUnitConstructorError {
public int square(int a) {
return a * a;
}
}
在类中,有一个 square() 方法用于返回一个数字的平方。
由于我们的主类已经准备好,现在让我们开始编写我们的测试类
public class ResolvingJUnitConstructorErrorUnitTest {
private int input;
public ResolvingJUnitConstructorErrorUnitTest(int input) {
this.input = input;
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
assertEquals(input * input, service.square(input));
}
}
这里,我们定义了一个 JUnit 4 测试,名为 ResolvingJUnitConstructorErrorUnitTest,它包含一个接受输入参数的构造函数。
最后,让我们尝试运行测试
$ mvn clean test
...
1. Test class should have exactly one public zero-argument constructor
...
一旦我们这样做,构建会在任何测试方法能够运行之前停止,最终我们得到预期的错误信息测试类应该只有一个公共无参数构造函数。
4. 在 JUnit 4 中解决错误
这里,让我们看看我们的第一个解决方案。在这个方案中,我们在 JUnit 4 中实现@RunWith(Parameterized.class)注解,以自动将参数传递给测试类构造函数
@RunWith(Parameterized.class)
public class ResolvingJUnitConstructorErrorUnitTest {
private final int input;
private final ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
public ResolvingJUnitConstructorErrorUnitTest(int input) {
this.input = input;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{2}, {3}, {4}
});
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
以上,我们更新我们的测试类以使用 JUnit 4 的参数化测试
- @RunWith(Parameterized.class):指示 JUnit 使用参数化运行器
- @Parameterized.Parameters:定义输入集合,在本例中为{2}, {3}, {4}
让我们看看再次运行测试会发生什么
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
...
所以,上面的输出显示我们的测试现在运行了三次。特别是,该测试针对每个输入值运行一次。对于每组数据({2}, {3}, {4}),JUnit 4 会创建测试类的一个新实例,并为每个实例单独运行测试,将它们视为独立执行。
使用@RunWith(Parameterized.class),JUnit 4 现在为每组输入数据创建测试类的一个新实例。每个实例通过我们定义的构造函数接收其参数。
这里,我们修复了该问题,并展示了 JUnit 4 如何通过基于构造函数的注入实现参数化测试。
5. 通过升级到 JUnit 5 解决问题
这里,让我们看看我们的第二个解决方案。在这个方案中,我们从 JUnit 4 升级到 JUnit 5
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.1</version>
<scope>test</scope>
</dependency>
</dependencies>
具体来说,我们添加了依赖项 JUnit Jupiter。
现在,让我们重写我们的测试
public class ResolvingJUnitConstructorErrorUnitTest {
private final ResolvingJUnitConstructorError service = new ResolvingJUnitConstructorError();
@ParameterizedTest
@ValueSource(ints = {2, 3, 4})
void givenNumber_whenSquare_thenReturnsCorrectResult(int input) {
assertEquals(input * input, service.square(input));
}
}
在更新后的示例中,JUnit 5 使用@ParameterizedTest 和@ValueSource 简化了参数化测试
- @ParameterizedTest:将测试标记为参数化
- @ValueSource(ints = {2, 3, 4}):定义内联测试数据
现在让我们运行测试
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
...
以上,输出显示测试运行了三次,与之前相同,并且所有测试都成功通过。每个输入都会触发一次单独的测试执行,确保测试运行之间的隔离,与 JUnit 4 相同。
默认情况下,JUnit 5 支持参数化测试而无需额外配置。因此,它消除了对公共无参数构造函数的需求,从而使我们的测试更加简洁易读。
6. 通过避免参数化构造函数解决问题
这里,让我们看看我们的第三个解决方案。在这个方案中,我们不在测试类中使用带参数的构造函数。具体来说,我们不通过构造函数传递值,而是在 setup 方法中初始化测试输入。
首先,让我们在 JUnit 4 中说明这一点
public class ResolvingJUnitConstructorErrorUnitTest {
private ResolvingJUnitConstructorError service;
private int input;
@Before
public void setUp() {
service = new ResolvingJUnitConstructorError();
input = 2;
}
@Test
public void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
现在,让我们看一个 JUnit 5 的例子
public class ResolvingJUnitConstructorErrorUnitTest {
private ResolvingJUnitConstructorError service;
private int input;
@BeforeEach
void setUp() {
service = new ResolvingJUnitConstructorError();
input = 2;
}
@Test
void givenNumber_whenSquare_thenReturnsCorrectResult() {
assertEquals(input * input, service.square(input));
}
}
在两个示例中,我们都使用了 setup 方法@Before(在 JUnit 4 中)和@BeforeEach(在 JUnit 5 中)。为了澄清,这两种方法都在每次测试运行之前初始化service 和input 字段。
现在,每个测试方法可以专注于断言预期的结果,而无需进行初始化。此外,我们可以将测试准备与执行分开,使测试方法专注于断言行为。
7. 常见错误和最佳实践
在重构或引入依赖关系的过程中,我们可能会不小心将带参数的构造函数添加到测试类中。在这种情况下,我们可以应用前面讨论的 JUnit 4 或 JUnit 5 解决方案来避免错误测试类应该只有一个公共无参数构造函数。
有时,我们可能会无意中重复类似的测试方法,这些方法在输入值上只有微小的变化。在这种情况下,我们可以考虑使用参数化测试而不是复制它们。
我们也可以继续使用 JUnit 4 编写测试,即使与替代方案相比可能需要更多的样板代码。当处理大型项目,可能需要编写许多测试时,迁移到 JUnit 5 可以更轻松地编写参数化测试并减少重复设置。
8. 结论
在本文中,我们检查了 JUnit 错误 测试类应该只有一个公共无参数构造函数。
该错误通常发生在 JUnit 4 中,当测试类定义了一个参数化构造函数时。在这种情况下,JUnit 4 无法在运行测试之前自动创建类的实例。为了演示,我们重现了这个问题,解释了它发生的原因,然后提供了实用的解决方案。
在我们的第一个解决方案中,我们使用 JUnit 4 中的参数化测试并控制测试数据的注入,而在我们的第二个解决方案中,我们升级到 JUnit 5 并提供了一种更简洁的方法。在我们的第三个解决方案中,我们消除了参数化构造函数的使用,并依赖于设置方法,这可以使我们的测试更简单且更易于维护。通过理解该错误,我们不仅可以帮助修复测试失败,还可以更深入地了解 JUnit 如何管理测试实例化和生命周期。
我们可以在 GitHub 上访问源代码。















