如何在 Java 中测试抽象超类
How to test an abstract superclass in Java
我是一名学习软件开发的学生(第一年),我们使用的教学语言是 Java。我们已经涵盖了基础知识和大部分 OOP,但我一直在练习制作商店管理系统,我遇到了一些我无法想象的事情。
我正在尝试 单元测试 两个 classes,它们 both abstract superclasses根据下面的 UML
,我计划实施的其他几个 classes
Person Superclass and Employee subclass - both abstract
我在这里阅读了一系列 post,我看到很多人都在推荐诸如 power mock 和 mockito 之类的东西来制作模拟对象。我可能试图一次学习太多东西,但基本上我在单元测试 class 中使用了具体的“包装器”私有 classes,我用来多态地创建 Employee 对象(技术上EmployeeWrapper 对象),然后通过包装器 class.
对所有 public 方法进行单元测试
我对“糟糕的代码味道”一词非常熟悉,这真的很糟糕。是否有一种标准方法可以在不使用 Mockito 和 Power Mock 之类的情况下测试抽象 superclasses?还是我只需要吸收它并使用类似的东西?
这是 classes 的代码(删除了所有方法体,因此您不必通读一大堆不重要的细节
import java.time.LocalDateTime;
import java.util.Hashtable;
import java.util.Iterator;
public abstract class Employee extends Person {
private double hourlyRate;
private double hoursPerWeek;
private LocalDateTime dateOfEmploymentStart;
private LocalDateTime dateOfEmploymentEnd;
private Hashtable<LocalDateTime, Integer> shifts;
private static final double MINIMUM_WAGE = 8.0;
/**
* Constructor for Employee for all fields except dateOfHire, which is set to {@code LocalDateTime.now()}
*
* @param name
* @param email
* @param phoneNumber
* @param hourlyRate
* @param weeklyHours
* @throws IllegalArgumentException if name if blank or null
*/
public Employee(String name, String email, String phoneNumber, double hourlyRate, double weeklyHours) throws IllegalArgumentException {
super(name, email, phoneNumber);
this.setHourlyRate(hourlyRate);
this.setWeeklyHours(weeklyHours);
this.setDateOfEmploymentStart(LocalDateTime.now());
this.shifts = new Hashtable<LocalDateTime, Integer>();
}
/**
* Constructor for Employee that sets name, email and phoneNumber to provided args; and sets hourly rate and weeklyHours to 0
*
* @param name
* @param email
* @param phoneNumber
* @throws IllegalArgumentException if name is blank or null
*/
public Employee(String name, String email, String phoneNumber) throws IllegalArgumentException {
this(name, email, phoneNumber, MINIMUM_WAGE, 0);
}
/**
* Constructor for Employee that sets only name
*
* @param name
* @throws IllegalArgumentException
*/
public Employee(String name) throws IllegalArgumentException {
this(name, null, null);
}
}
和单元测试 class(所有测试用例都删除了一个,并且那个方法主体留空 - 再次停止混乱:
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDateTime;
import java.util.Hashtable;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class EmployeeTest {
private class EmployeeWrapper extends Employee {
public EmployeeWrapper(String name, String email, String phoneNumber, double hourlyRate, double weeklyHours) throws IllegalArgumentException {
super(name, email, phoneNumber, hourlyRate, weeklyHours);
}
public EmployeeWrapper(String name, String email, String phoneNumber) throws IllegalArgumentException {
super(name, email, phoneNumber);
}
public EmployeeWrapper(String name) throws IllegalArgumentException {
super(name);
}
}
private String nameValid, emailValid, phoneNumberValid;
private String nameInvalid, emailInvalid, phoneNumberInvalid;
private double hourlyRateValid, hourlyRateInvalidLow;
private double weeklyHoursValid, weeklyHoursInvalid;
private final double DEFAULT_HOURLY_RATE = 8;
private final double DEFAULT_WEEKLY_HOURS = 0;
private final String DEFAULT_EMAIL = "no email provided";
private final String DEFAULT_PHONE_NUMBER = "no phone number provided";
private final double MINIMUM_WAGE = 8.0;
private Employee employee;
private Hashtable<LocalDateTime, Integer> shiftsValid, shiftsInvalidEmpty;
private LocalDateTime dateTimeValid, dateTimePast, dateTimeFuture;
@BeforeEach
void setUp() throws Exception {
// valid employee
nameValid = "testname";
phoneNumberValid = "123456789";
emailValid = "test@test.test.com";
hourlyRateValid = 10.50;
weeklyHoursValid = 7.5;
employee = new EmployeeWrapper(nameValid, emailValid, phoneNumberValid, hourlyRateValid, weeklyHoursValid);
// test data
nameInvalid = "";
emailInvalid = ".test@test.com";
phoneNumberInvalid = "";
hourlyRateInvalidLow = 5;
weeklyHoursInvalid = -10;
dateTimeValid = LocalDateTime.of(2015, 6, 15, 13, 30);
dateTimePast = LocalDateTime.MIN;
dateTimeFuture = LocalDateTime.MAX;
shiftsValid = new Hashtable<LocalDateTime, Integer>();
shiftsValid.put(dateTimeValid, 6);
shiftsValid.put(dateTimeFuture, 3);
shiftsInvalidEmpty = new Hashtable<LocalDateTime, Integer>();
}
@Test
void testEmployeeConstructorValidAllArgs() {
}
}
这是我第一次 post Stack Overflow,所以如果我遗漏了任何相关细节,我深表歉意。
如果你看到我在代码中做的任何其他愚蠢的事情,我也很乐意接受任何批评!
编辑:感谢大家的回复,他们太棒了,我真的很感激!
如您所述,您无法在 Java 中测试摘要 classes。您需要一个像 Mockito 这样的模拟框架或一个扩展您的 superclass 的具体 class,在本例中为 Employee
。
这就是您对 class EmployeeWrapper
所做的。除了名字(我会把它命名为 EmployeeImpl
)我对你的解决方案很好。
首先让我说,你的方法是绝对可行的。我只是分享我自己的做法,因为它避免了不同实现之间常见的复制粘贴测试。
我没有专门测试摘要 classes。因为我们正在测试功能,它可以在 subclasses 中被覆盖。我将使用您的 Person
class 进行此设置,但我会稍微简化一下。
public abstract class Person {
private String name;
private String email;
public Person(String name, String email) {
this.setName(name);
this.email = email;
}
public String getName() {
return this.name;
}
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("missing name");
}
this.name = name;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}
学生
public class Student extends Person {
private String university;
public Student(String name, String email, String university) {
super(name, email);
this.university = university;
}
public String getUniversity() {
return this.university;
}
public void setUniversity(String university) {
this.university = university;
}
}
Child
public class Child extends Person {
private String school;
public Child(String name, String email, String school) {
super(name, email);
this.school = school;
}
public String getSchool() {
return this.school;
}
public void setSchool(String school) {
this.school = school;
}
@Override
public String getName() {
return "I am not saying!";
}
}
所以我们有一个抽象的人,一个Student
,它的具体事物是一所大学和一个Child
。拥有一所学校是 child 所特有的,但它也改变了 getName()
的行为,它没有透露它的名字。这可能是需要的,但对于这个例子,我们假设像这样覆盖 getName()
是不正确的。
在处理抽象 classes 时,我做了一个抽象测试 class,它包含抽象 class - Person
提供的通用功能的通用设置和测试在这种情况下。
public abstract class PersonBaseTests {
protected static final String EXPECTED_NAME = "George";
private Person person;
@BeforeEach
public void setUp() {
this.person = getConcretePersonImplementation();
}
/**
* @return new instance of non-abstract class extending person
*/
protected abstract Person getConcretePersonImplementation();
//common tests
@Test
public void testGetName_ShouldReturnCorrectValue() {
assertEquals(EXPECTED_NAME, this.person.getName());
}
@Test
public void testConstructor_ShouldThrowIllegalArgumentExceptionOnMissingName() {
Executable invalidConstructorInvocation = getConstructorExecutableWithMissingName();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, invalidConstructorInvocation);
assertEquals("missing name", exception.getMessage());
}
protected abstract Executable getConstructorExecutableWithMissingName();
//other common tests
}
扩展基础的测试classes 必须提供要测试的具体实现。它们还将继承测试,因此您无需再次编写它们。如果您还没有了解接口、lambda 和类似的东西,您可以 忽略 构造函数异常测试和与之相关的所有内容,并专注于 getName()
测试。它测试 getter 正确 returns Person
的名称。对于 Child
,这显然会失败,但这就是想法。您可以添加用于获取和设置电子邮件、phone 等的测试
所以,学生测试
public class StudentTests extends PersonBaseTests {
@Override
protected Person getConcretePersonImplementation() {
return new Student(PersonBaseTests.EXPECTED_NAME, "mail", "Cambridge");
}
@Override
protected Executable getConstructorExecutableWithMissingName() {
//setup invocation which will actually fail
return new StudentConstructorExecutable(null, "email@email.email", "Stanford");
}
private static final class StudentConstructorExecutable implements Executable {
private final String name;
private final String email;
private final String university;
private StudentConstructorExecutable(String name, String email, String university) {
this.name = name;
this.email = email;
this.university = university;
}
@Override
public void execute() throws Throwable {
//this will invoke the constructor with values from fields
new Student(this.name, this.email, this.university);
}
}
//write tests specific for student class
//getUniversity() tests for example
}
同样,如果您还没有学会,请忽略 Executable
和所有与构造函数测试相关的内容。学生测试为常见的继承测试提供了 Student
的具体实例,您可以为特定功能编写额外的测试 - get/set university.
Child 测试
public class ChildTests extends PersonBaseTests {
@Override
protected Person getConcretePersonImplementation() {
return new Child(PersonBaseTests.EXPECTED_NAME, "", "some school");
}
@Override
protected Executable getConstructorExecutableWithMissingName() {
//this can be ignored
return () -> new Child(null, "", "");
}
//write tests specific for child class
//getSchool() tests for example
}
再次为常见测试提供了一个具体实例 - 这次是 Child
类型。您可以为 Child
class 提供的任何附加功能添加测试 - 在本例中获取和设置学校。然后你可以为 Person
.
的每一个额外的 subclass 编写更多测试 classes
像这样,您将通用测试放在一个地方,并且您编写的抽象 class 的每个具体实现都经过完整测试,没有测试重复。关于失败的测试,如果 getName()
的行为变化是有意的,您可以在 ChildTests
中覆盖它以将其考虑在内。如果不是故意的,你知道,Student.getName()
是正确的,而 Child.getName()
不是,但你只写了一次测试。
我是一名学习软件开发的学生(第一年),我们使用的教学语言是 Java。我们已经涵盖了基础知识和大部分 OOP,但我一直在练习制作商店管理系统,我遇到了一些我无法想象的事情。
我正在尝试 单元测试 两个 classes,它们 both abstract superclasses根据下面的 UML
,我计划实施的其他几个 classesPerson Superclass and Employee subclass - both abstract
我在这里阅读了一系列 post,我看到很多人都在推荐诸如 power mock 和 mockito 之类的东西来制作模拟对象。我可能试图一次学习太多东西,但基本上我在单元测试 class 中使用了具体的“包装器”私有 classes,我用来多态地创建 Employee 对象(技术上EmployeeWrapper 对象),然后通过包装器 class.
对所有 public 方法进行单元测试我对“糟糕的代码味道”一词非常熟悉,这真的很糟糕。是否有一种标准方法可以在不使用 Mockito 和 Power Mock 之类的情况下测试抽象 superclasses?还是我只需要吸收它并使用类似的东西?
这是 classes 的代码(删除了所有方法体,因此您不必通读一大堆不重要的细节
import java.time.LocalDateTime;
import java.util.Hashtable;
import java.util.Iterator;
public abstract class Employee extends Person {
private double hourlyRate;
private double hoursPerWeek;
private LocalDateTime dateOfEmploymentStart;
private LocalDateTime dateOfEmploymentEnd;
private Hashtable<LocalDateTime, Integer> shifts;
private static final double MINIMUM_WAGE = 8.0;
/**
* Constructor for Employee for all fields except dateOfHire, which is set to {@code LocalDateTime.now()}
*
* @param name
* @param email
* @param phoneNumber
* @param hourlyRate
* @param weeklyHours
* @throws IllegalArgumentException if name if blank or null
*/
public Employee(String name, String email, String phoneNumber, double hourlyRate, double weeklyHours) throws IllegalArgumentException {
super(name, email, phoneNumber);
this.setHourlyRate(hourlyRate);
this.setWeeklyHours(weeklyHours);
this.setDateOfEmploymentStart(LocalDateTime.now());
this.shifts = new Hashtable<LocalDateTime, Integer>();
}
/**
* Constructor for Employee that sets name, email and phoneNumber to provided args; and sets hourly rate and weeklyHours to 0
*
* @param name
* @param email
* @param phoneNumber
* @throws IllegalArgumentException if name is blank or null
*/
public Employee(String name, String email, String phoneNumber) throws IllegalArgumentException {
this(name, email, phoneNumber, MINIMUM_WAGE, 0);
}
/**
* Constructor for Employee that sets only name
*
* @param name
* @throws IllegalArgumentException
*/
public Employee(String name) throws IllegalArgumentException {
this(name, null, null);
}
}
和单元测试 class(所有测试用例都删除了一个,并且那个方法主体留空 - 再次停止混乱:
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDateTime;
import java.util.Hashtable;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class EmployeeTest {
private class EmployeeWrapper extends Employee {
public EmployeeWrapper(String name, String email, String phoneNumber, double hourlyRate, double weeklyHours) throws IllegalArgumentException {
super(name, email, phoneNumber, hourlyRate, weeklyHours);
}
public EmployeeWrapper(String name, String email, String phoneNumber) throws IllegalArgumentException {
super(name, email, phoneNumber);
}
public EmployeeWrapper(String name) throws IllegalArgumentException {
super(name);
}
}
private String nameValid, emailValid, phoneNumberValid;
private String nameInvalid, emailInvalid, phoneNumberInvalid;
private double hourlyRateValid, hourlyRateInvalidLow;
private double weeklyHoursValid, weeklyHoursInvalid;
private final double DEFAULT_HOURLY_RATE = 8;
private final double DEFAULT_WEEKLY_HOURS = 0;
private final String DEFAULT_EMAIL = "no email provided";
private final String DEFAULT_PHONE_NUMBER = "no phone number provided";
private final double MINIMUM_WAGE = 8.0;
private Employee employee;
private Hashtable<LocalDateTime, Integer> shiftsValid, shiftsInvalidEmpty;
private LocalDateTime dateTimeValid, dateTimePast, dateTimeFuture;
@BeforeEach
void setUp() throws Exception {
// valid employee
nameValid = "testname";
phoneNumberValid = "123456789";
emailValid = "test@test.test.com";
hourlyRateValid = 10.50;
weeklyHoursValid = 7.5;
employee = new EmployeeWrapper(nameValid, emailValid, phoneNumberValid, hourlyRateValid, weeklyHoursValid);
// test data
nameInvalid = "";
emailInvalid = ".test@test.com";
phoneNumberInvalid = "";
hourlyRateInvalidLow = 5;
weeklyHoursInvalid = -10;
dateTimeValid = LocalDateTime.of(2015, 6, 15, 13, 30);
dateTimePast = LocalDateTime.MIN;
dateTimeFuture = LocalDateTime.MAX;
shiftsValid = new Hashtable<LocalDateTime, Integer>();
shiftsValid.put(dateTimeValid, 6);
shiftsValid.put(dateTimeFuture, 3);
shiftsInvalidEmpty = new Hashtable<LocalDateTime, Integer>();
}
@Test
void testEmployeeConstructorValidAllArgs() {
}
}
这是我第一次 post Stack Overflow,所以如果我遗漏了任何相关细节,我深表歉意。
如果你看到我在代码中做的任何其他愚蠢的事情,我也很乐意接受任何批评!
编辑:感谢大家的回复,他们太棒了,我真的很感激!
如您所述,您无法在 Java 中测试摘要 classes。您需要一个像 Mockito 这样的模拟框架或一个扩展您的 superclass 的具体 class,在本例中为 Employee
。
这就是您对 class EmployeeWrapper
所做的。除了名字(我会把它命名为 EmployeeImpl
)我对你的解决方案很好。
首先让我说,你的方法是绝对可行的。我只是分享我自己的做法,因为它避免了不同实现之间常见的复制粘贴测试。
我没有专门测试摘要 classes。因为我们正在测试功能,它可以在 subclasses 中被覆盖。我将使用您的 Person
class 进行此设置,但我会稍微简化一下。
public abstract class Person {
private String name;
private String email;
public Person(String name, String email) {
this.setName(name);
this.email = email;
}
public String getName() {
return this.name;
}
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("missing name");
}
this.name = name;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}
学生
public class Student extends Person {
private String university;
public Student(String name, String email, String university) {
super(name, email);
this.university = university;
}
public String getUniversity() {
return this.university;
}
public void setUniversity(String university) {
this.university = university;
}
}
Child
public class Child extends Person {
private String school;
public Child(String name, String email, String school) {
super(name, email);
this.school = school;
}
public String getSchool() {
return this.school;
}
public void setSchool(String school) {
this.school = school;
}
@Override
public String getName() {
return "I am not saying!";
}
}
所以我们有一个抽象的人,一个Student
,它的具体事物是一所大学和一个Child
。拥有一所学校是 child 所特有的,但它也改变了 getName()
的行为,它没有透露它的名字。这可能是需要的,但对于这个例子,我们假设像这样覆盖 getName()
是不正确的。
在处理抽象 classes 时,我做了一个抽象测试 class,它包含抽象 class - Person
提供的通用功能的通用设置和测试在这种情况下。
public abstract class PersonBaseTests {
protected static final String EXPECTED_NAME = "George";
private Person person;
@BeforeEach
public void setUp() {
this.person = getConcretePersonImplementation();
}
/**
* @return new instance of non-abstract class extending person
*/
protected abstract Person getConcretePersonImplementation();
//common tests
@Test
public void testGetName_ShouldReturnCorrectValue() {
assertEquals(EXPECTED_NAME, this.person.getName());
}
@Test
public void testConstructor_ShouldThrowIllegalArgumentExceptionOnMissingName() {
Executable invalidConstructorInvocation = getConstructorExecutableWithMissingName();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, invalidConstructorInvocation);
assertEquals("missing name", exception.getMessage());
}
protected abstract Executable getConstructorExecutableWithMissingName();
//other common tests
}
扩展基础的测试classes 必须提供要测试的具体实现。它们还将继承测试,因此您无需再次编写它们。如果您还没有了解接口、lambda 和类似的东西,您可以 忽略 构造函数异常测试和与之相关的所有内容,并专注于 getName()
测试。它测试 getter 正确 returns Person
的名称。对于 Child
,这显然会失败,但这就是想法。您可以添加用于获取和设置电子邮件、phone 等的测试
所以,学生测试
public class StudentTests extends PersonBaseTests {
@Override
protected Person getConcretePersonImplementation() {
return new Student(PersonBaseTests.EXPECTED_NAME, "mail", "Cambridge");
}
@Override
protected Executable getConstructorExecutableWithMissingName() {
//setup invocation which will actually fail
return new StudentConstructorExecutable(null, "email@email.email", "Stanford");
}
private static final class StudentConstructorExecutable implements Executable {
private final String name;
private final String email;
private final String university;
private StudentConstructorExecutable(String name, String email, String university) {
this.name = name;
this.email = email;
this.university = university;
}
@Override
public void execute() throws Throwable {
//this will invoke the constructor with values from fields
new Student(this.name, this.email, this.university);
}
}
//write tests specific for student class
//getUniversity() tests for example
}
同样,如果您还没有学会,请忽略 Executable
和所有与构造函数测试相关的内容。学生测试为常见的继承测试提供了 Student
的具体实例,您可以为特定功能编写额外的测试 - get/set university.
Child 测试
public class ChildTests extends PersonBaseTests {
@Override
protected Person getConcretePersonImplementation() {
return new Child(PersonBaseTests.EXPECTED_NAME, "", "some school");
}
@Override
protected Executable getConstructorExecutableWithMissingName() {
//this can be ignored
return () -> new Child(null, "", "");
}
//write tests specific for child class
//getSchool() tests for example
}
再次为常见测试提供了一个具体实例 - 这次是 Child
类型。您可以为 Child
class 提供的任何附加功能添加测试 - 在本例中获取和设置学校。然后你可以为 Person
.
像这样,您将通用测试放在一个地方,并且您编写的抽象 class 的每个具体实现都经过完整测试,没有测试重复。关于失败的测试,如果 getName()
的行为变化是有意的,您可以在 ChildTests
中覆盖它以将其考虑在内。如果不是故意的,你知道,Student.getName()
是正确的,而 Child.getName()
不是,但你只写了一次测试。