Статьи

Что следует тестировать? — Методы испытаний 3

Вчера я был в офисе, разговаривая о тестировании с одним из моих коллег, который был немного убежден в написании модульных тестов. Одна из причин, по которой он использовал это, заключалась в том, что некоторые тесты кажутся бессмысленными, что подводит меня к вопросу о том, что именно вы делаете, и что вам не нужно беспокоиться.

Рассмотрим простой неизменяемый бин 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