Вчера я был в офисе, разговаривая о тестировании с одним из моих коллег, который был немного убежден в написании модульных тестов. Одна из причин, по которой он использовал это, заключалась в том, что некоторые тесты кажутся бессмысленными, что подводит меня к вопросу о том, что именно вы делаете, и что вам не нужно беспокоиться.
Рассмотрим простой неизменяемый бин 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