Вчера я был в офисе, разговаривая о тестировании с одним из моих коллег, который был немного убежден в написании модульных тестов. Одна из причин, по которой он использовал это, заключалась в том, что некоторые тесты кажутся бессмысленными, что подводит меня к вопросу о том, что именно вы делаете, и что вам не нужно беспокоиться.
Рассмотрим простой неизменяемый бин Name с конструктором и несколькими получателями. В этом примере я собираюсь позволить коду говорить за себя, так как я надеюсь, что очевидно, что любое тестирование будет бессмысленным.
public class Name {
private final String firstName;
private final String middleName;
private final String surname;
public Name(String christianName, String middleName, String surname) {
this.firstName = christianName;
this.middleName = middleName;
this.surname = surname;
}
public String getFirstName() {
return firstName;
}
public String getMiddleName() {
return middleName;
}
public String getSurname() {
return surname;
}
}
… и чтобы подчеркнуть это, вот бессмысленный тестовый код:
public class NameTest {
private Name instance;
@Before
public void setUp() {
instance = new Name("John", "Stephen", "Smith");
}
@Test
public void testGetFirstName() {
String result = instance.getFirstName();
assertEquals("John", result);
}
@Test
public void testGetMiddleName() {
String result = instance.getMiddleName();
assertEquals("Stephen", result);
}
@Test
public void testGetSurname() {
String result = instance.getSurname();
assertEquals("Smith", result);
}
}
Причиной бессмысленного тестирования этого класса является то, что код не содержит никакой логики; однако в тот момент, когда вы добавляете что-то вроде этого:
public String getFullName() {
if (isValidString(firstName) && isValidString(middleName) && isValidString(surname)) {
return firstName + " " + middleName + " " + surname;
} else {
throw new RuntimeException("Invalid Name Values");
}
}
private boolean isValidString(String str) {
return isNotNull(str) && str.length() > 0;
}
private boolean isNotNull(Object obj) {
return obj != null;
}
… для вашего класса, тогда вся ситуация меняется. Добавление некоторой логики в виде оператора if создает целую кучу тестов:
@Test
public void testGetFullName_with_valid_input() {
instance = new Name("John", "Stephen", "Smith");
final String expected = "John Stephen Smith";
String result = instance.getFullName();
assertEquals(expected, result);
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_null_firstName() {
instance = new Name(null, "Stephen", "Smith");
instance.getFullName();
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_null_middleName() {
instance = new Name("John", null, "Smith");
instance.getFullName();
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_null_surname() {
instance = new Name("John", "Stephen", null);
instance.getFullName();
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_no_firstName() {
instance = new Name("", "Stephen", "Smith");
instance.getFullName();
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_no_middleName() {
instance = new Name("John", "", "Smith");
instance.getFullName();
}
@Test(expected = RuntimeException.class)
public void testGetFullName_with_no_surname() {
instance = new Name("John", "Stephen", "");
instance.getFullName();
}
Итак, учитывая, что я только что сказал, что вам не нужно проверять объекты, которые не содержат никаких логических операторов, и в список
логических операторов я бы включил if и переключился вместе со всеми операторами (+ — * — ), и целый набор вещей, которые могут измениться и объекты состояния.
Учитывая эту предпосылку, я бы тогда предположил, что бессмысленно писать модульный тест для объекта доступа к адресным данным (DAO) в проекте Address
1, о котором я говорил в последних двух блогах. DAO определяется интерфейсом AddressDao и реализуется классом JdbcAddress:
public class JdbcAddress extends JdbcDaoSupport implements AddressDao {
/**
* This is an instance of the query object that'll sort out the results of
* the SQL and produce whatever values objects are required
*/
private MyQueryClass query;
/** This is the SQL with which to run this DAO */
private static final String sql = "select * from addresses where id = ?";
/**
* A class that does the mapping of row data into a value object.
*/
class MyQueryClass extends MappingSqlQuery<Address> {
public MyQueryClass(DataSource dataSource, String sql) {
super(dataSource, sql);
this.declareParameter(new SqlParameter(Types.INTEGER));
}
/**
* This the implementation of the MappingSqlQuery abstract method. This
* method creates and returns a instance of our value object associated
* with the table / select statement.
*
* @param rs
* This is the current ResultSet
* @param rowNum
* The rowNum
* @throws SQLException
* This is taken care of by the Spring stuff...
*/
@Override
protected Address mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Address(rs.getInt("id"), rs.getString("street"),
rs.getString("town"), rs.getString("post_code"),
rs.getString("country"));
}
}
/**
* Override the JdbcDaoSupport method of this name, calling the super class
* so that things get set-up correctly and then create the inner query
* class.
*/
@Override
protected void initDao() throws Exception {
super.initDao();
query = new MyQueryClass(getDataSource(), sql);
}
/**
* Return an address object based upon it's id
*/
@Override
public Address findAddress(int id) {
return query.findObject(id);
}
}
В приведенном выше коде единственный метод в интерфейсе:
@Override
public Address findAddress(int id) {
return query.findObject(id);
}
… который действительно простой метод получения. Мне кажется, что это нормально, поскольку в DAO, который входит в AddressService, не должно быть какой-либо бизнес-логики, у которой должно быть множество модульных тестов.
Вы можете решить, хотите ли вы писать модульные тесты для MyQueryClass. Для меня это пограничный случай, поэтому я с нетерпением жду каких-либо комментариев …
Я предполагаю, что кто-то не согласится с этим подходом, скажем, вам следует протестировать объект JdbcAddress, и это правда, я лично написал бы
для него интеграционный тест, чтобы убедиться, что с базой данных, с которой я работаю, все в порядке, что она понимает мою SQL и то, что эти два объекта (DAO и база данных) могут общаться друг с другом, но я не буду беспокоиться о
модульном тестировании .
В заключение, модульные тесты должны быть значимыми, и хорошее определение «значимых» состоит в том, что тестируемый объект должен содержать некоторую независимую логику.
1 Исходный код доступен на GitHub по адресу:
git: //github.com/roghughe/captaindebug.git
С http://www.captaindebug.com/2011/11/what-should-you-unit-test-testing.html