06 Aug 2014

Spock Lifecycle and non-intuitive WHERE block

Résumé: as it appeared Spock’s Lifecycle is not always intuitive, this is especially true for where block. Even though it’s located inside of a test method, its execution happens outside and what’s even more peculiar - it’s invoked even before setup() happens or fields get initialized.

Spock allows you writing data-driven tests in a very easy manner using where:

def "square function"(int a, int square) {
        expect:
            square == a * a
        where:
            a | square
           -1 | 1
            0 | 0
            1 | 1
}

But docs keep silence on how this magic is implemented. Spock’s Syntax is not pure Groovy, its code gets transformed with Groovy AST. Depending on the result of these transformations we may or may not use some features of Spock.

First, where is going to be factored out into a separate method within the same test class. But what’s really interesting - it will be invoked before setup(). The whole thing is here:

This means that variables that are initialized in setup() won’t be seen in where:

class SomeTest extends Specification {
    int a = 2
    int b = 1

    def setup() {
        b = 1
    }
    def someTest(a, b){
      expect:
        b != a
      where:
        a << [a]
        b << [b]
    }
}

In this test we’ll get a == b == 0 which demonstrates the point.

As a consequence all the JUnit Listeners (after all Spock is just an extension of JUnit) that observe beforeTestMethod will also be triggered after the data is initialized in where. This includes DependencyInjectionTestExecutionListener from Spring TestContext. Which means that if you want to use @Autowired fields which are injected from the context, you won’t be able to use them in where, what you’ll get there is null.

Read more
18 Jun 2014

Spock Lifecycle и неинтуитивный блок where

Резюме: как оказывается у Spock’а порою не совсем интуитивный жизненный цикл, особенно это касается блока where который хоть и находится внутри тестового метода, а выполняется не то что до самого метода (что было бы логично), а даже до setup() и до инициализации полей класса.

Spock позволяет достаточно удобно строить data-driven тесты с помощью своего where:

def "square function"(int a, int square) {
        expect:
            square == a * a
        where:
            a | square
           -1 | 1
            0 | 0
            1 | 1
}

Нигде только не упоминается о том как же в итоге это все реализуется. Синтаксис используемый Spock’ом на самом деле не является чистым Groovy, этот код потом трансформируется с помощью Groovy AST. Так вот от того во что он трансформируется зависит возможность использования некоторых фич Spock’a.

Так, например, where будет вынесено отдельным методом в тот же класс и, что интересно, будет вызван до setup(). Цепочку можно проследить тут:

Это значит так же что переменные которые инициализируются в setup() не будут видны в where:

class SomeTest extends Specification {
    int a = 2
    int b = 1

    def setup() {
        b = 1
    }
    def someTest(a, b){
      expect:
        b != a
      where:
        a << [a]
        b << [b]
    }
}

И в итоге мы все равно получим в тесте a == b == 0.

Из этого так же следует, что все слушатели, которые навешаны на beforeTestMethod тоже отработают после того как данные будут инициализированы в where. К таким слушателям относятся и DependencyInjectionTestExecutionListener из Spring TestContext. Т.е. если вы захотите использовать @Autowired поля которые инжектятся из контекста, в where вы их использовать не сможете, там у вас все равно будут null.

Read more
07 Nov 2013

Рецензия на JBehave

Резюме: в последнее время все большей популярностью начинают пользоваться BDD фреймворки для тестирования, однако не каждый из них так хорош как кажется.

О BDD тестах

Сначала поясню что такое BDD (Behaviour Driven Development) и какая идея стоит за BDD тестами. Так вот, требования можно записывать по-разному, и одной из форм является Given-When-Then стиль, например:

Given User is registered and logged in, when she posts an article, article count gets incremented.

Идея BDD тестов в том, чтоб использовать такое требование как тест сценарий. Таким образом если мы автоматизируем его проверку, то это одновременно станет как нашей документацией, так и достаточно читаемым тестом.

Как JBehave реализует BDD тесты

Сначала описывается сценарий в простом текстовом файле называемом Историей:

Scenario: When user write an article, her article count increments

Given user is registered
And user is logged in
When user posts an article
Then user article count is incremented

Каждый из этих шагов мапится на метод в так называемых Step’ах в Java классах:

@Given("user is registered")
public void userIsRegistered(){}

@Given("user is logged in")
public void userIsLoggedIn(){}

@When("user posts an article")
public void writeAnArticle() {}

@Then("user article count is incremented")
public void assertArticleCountIncremented(){}

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

Проблемы JBehave

Неожиданно, когда начинаешь сам писать тесты, сталкиваешься с двумя проблемами, о которых сначала и не думал.

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

Во-вторых, у сценария нет контекста! Ведь никто не подумал а как мы создадим пользователя в первом шаге, а затем будем им же логиниться во втором и писать статьи в третьем. А оказывается между шагами нужно хранить состояние, но JBehave (кстати как и Fitnesse) при всем количестве фич не поддерживает этого. И приходится вставлять очень болезненные костыли - хранить состояние каждого тест кейса в глобальном хранилище (мапа?). А ведь все не так просто - один и тот же шаг может переиспользоваться в разных сценариях, и предыдущие шаги в этих сценариях могут быть разными - они могут как создавать какое-то состояние, так и не делать этого. И как в нашем текущем шаге определить намеревался ли предыдущий метод сохранить данные или нет?

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

Альтернативы

Сначала стоит заметить что на голом TestNG тесты выходят намного компактней и приятней, хоть и об отчетах приходится думать “вручную”.

А еще есть с первого взгляда вкусные фреймворки, которые лишены перечисленных недостатков, такие как основанный на Groovy easyb:

scenario "null is pushed onto empty stack", {
  given "an empty stack",{
    stack = new Stack()
  }

  when "null is pushed", {
    pushnull = {
      stack.push(null)
    }
  }

  then "an exception should be thrown", {
    ensureThrows(RuntimeException){
      pushnull()
    }
  }

  and "then the stack should still be empty", {
    stack.empty.shouldBe true
  }
}

Снова Groovy-based фреймворк Spock:

def "HashMap accepts null key"() {
  setup:
  def map = new HashMap()
  
  when:
  map.put(null, "elem")
  
  then:
  notThrown(NullPointerException)
}

Сам я с этими фреймворками не работал и не знаю их недостатков, но как минимум у них есть test case scope.

Read more
03 Jul 2013

Testing Worst Practices

Резюме: при достаточной популярности написания тестов (особенно модульных), появляются очень разнообразные “лучшие практики” в связи с этим. Однако похоже многие разработчики (даже очень опытные) все-таки не понимают как не нужно писать тесты. Поэтому поговорим о worst practices в тестировании. Все примеры в статье - из настоящих проектов.

Лирическое отступление

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

Проблем с тестами может быть три: их трудно поддерживать, читать и писать. Каждая проблема может решаться по-своему, во многих случаях достаточно просто порефакторить тесты, сделать их более грамотными. Бывает этого недостаточно и нужно рефакторить сам код - не имея адекватного кода писать тесты достаточно проблематично. Разработчики же в погоне за “лучшими практиками” забывают, что главное - это простота и эффективность, поэтому хочу показать во что выливается слепое следование идеалам из интернетов.

Прежде чем начнем рассматривать конкретные случаи, - немного терминологии:

  • SUT - system under test, то бишь то, что мы тестируем. Бывает еще называют CUT (component under test). В случае модульных тестов это как правило тестируемый класс.

  • DOC - depended on objects, то бишь то, от чего зависит SUT. Например, класс может использовать другой класс делегируя часть функционала. Этот делегат и называют DOC’ом.

  • Тест - в данном контексте это тестовый метод, тест кейс. Не класс! Тестовый класс - это набор тестов.

Практика

Итак, не тестируйте несколько результатов. Если функция затрагивает несколько аспектов, то у разработчиков руки чешутся протестировать их все в одном тесте, к примеру:

@Test
    public void testTopicChanged() throws MailingFailedException {
        prepareEnabledProperty();
        topic.getSubscribers().add(user1);
        topic.getSubscribers().add(user2);
        topic.getSubscribers().add(currentUser);
        when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers());

        service.subscribedEntityChanged(topic);

        verify(mailService, times(2)).sendUpdatesOnSubscription(any(JCUser.class), eq(topic));
        verify(mailService).sendUpdatesOnSubscription(user1, topic);
        verify(mailService).sendUpdatesOnSubscription(user2, topic);
        assertEquals(topic.getSubscribers().size(), 3);
    }

В данном тесте проверяется, что подписанные пользователи получают письмо, а неподписанные его не получают. Недостаток такого подхода в том, что когда упадет тест будет не понятно что не работает - нужно лезть в сам тест и смотреть его код. Если же было бы два теста: subscribedUserShouldReceiveNotification & notSubscribedUserShouldNotBeNotified тогда при красном тесте ясно ЧТО не работает.

Плюс сам тест становится сложней. Много маленьких тестов с короткими сценариями намного лучше, чем мало, но с длинной простыней.

Не проверяйте вызовы методов. Ваша SUT должна вернуть результат. И только его (этот результат) нужно проверять. Как не надо:

...
when(userDao.getByUsername(username)).thenReturn(user);
...
boolean isAuthenticated = userService.loginUser(username, PASSWORD, false, httpRequest, httpResponse);
assertTrue(isAuthenticated);
verify(userDao).getByUsername(username);

В данном случае вызов userDao.getByUsername() является внутренним устройством SUT. Нам не надо знать об этом, нам важен результат. Дабы вообще было возможно протестировать SUT нам приходится лезть своими грязными руками в реализацию и определять поведение DOC’a - но это плохо. Что еще хуже - мы на этом не останавливаемся и проверяем, что взаимодействие с DOC’ом на самом деле произошло. Но зачем, является ли это результатом функции? Это личная жизнь SUT, не нужно в нее вмешиваться ибо при рефакторинге нам многое придется в тестах переписывать. Если хотите писать поддерживаемые тесты, избегайте проверок вызовов.

Исключение составляет тот случай, когда взаимодействие с DOC’ом собсно является результатом выполнения функции. К примеру, оповещение пользователей по мейлу о каком-то событии - это результат действий SUT. К сожалению без влезания во внутренности реализации SUT в таком случае не всегда можно обойтись.

Не усложняйте тестовые данные “на всякий случай”. Наглядней на примере:

@Test
    public void extractMentionedUserShouldReturnAllMentionedCyrillicUserInBBCodes() {
        String textWithUsersMentioning = "In this text we have 3 user mentioning: first [user]Иванов[/user]," +
                "second [user notified=true]Петров[/user]," +
                "third [user]Сидоров[/user]";

        MentionedUsers mentionedUsers = MentionedUsers.parse(textWithUsersMentioning);
        Set<String> extractedUserNames = mentionedUsers.extractAllMentionedUsers(textWithUsersMentioning);

        assertTrue(extractedUserNames.size() == 3, "Passed text should contain 3 user mentioning.");
        assertTrue(extractedUserNames.contains("Иванов"), "Иванов is mentioned, so he should be extracted.");
        assertTrue(extractedUserNames.contains("Петров"), "Петров is mentioned, so he should be extracted.");
        assertTrue(extractedUserNames.contains("Сидоров"), "Сидоров is mentioned, so he should be extracted.");
    }

Данный тест должен проверить, что метод может работать с русскими пользователями. Однако он проверяет сразу 3х, а не одного! Знаю о чем думал разработчик, и возможно подумали вы, - на всякий случай, авось проблема обнаружится когда пользователей несколько. Но задача тестов - предоставлять гарантии, а не тыкать пальцем в небо. Гарантии предоставляются конкретными тест кейсами, которые проверяют вполне конкретные случаи. Это раз. А два - кто сказал, что с одним пользователем не сломается? Если хотите проверить случай - добавьте тест специально для этого случая, но не мешайте все вместе - лишь переводите чужое и свое время.

Другое, о чем может подумать разработчик, когда пишет такое - потестирую-ка я и другие значения, может на другом значении упадет! Но эта логика тоже не верна, у QA есть несколько методик при написании тест кейсов. Одна из них - это разбиение тестовых данных на классы эквивалентности. Суть в том, что весь набор данных делится на группы, и мы считаем, что данные из одной и той же группы вряд ли приведут к ошибке. Пока у вас не будет четкого разбиения данных на группы, ваши тесты пишутся на авось. Не пишите лишние тесты для одного и того же класса эквивалентности.

Не используйте DataProvider’ы. Похожих результатов в JUnit можно достигнув с помощью @Parameterized и @Theory. Они позволяют задать какие-то тестовые данные в одном месте сразу кучей, а затем использовать один тестовый метод для работы с этими данными:

@DataProvider(name = "status-provider")
    public Object[][] statusData() {
        return new Object[][]{
                {PrivateMessageStatus.NEW, false},
                {PrivateMessageStatus.DRAFT, false},
                {PrivateMessageStatus.SENT, true},
                {PrivateMessageStatus.DELETED_FROM_INBOX, false},
                {PrivateMessageStatus.DELETED_FROM_OUTBOX, true}
        };
    }

Этот метод предоставляет данные и результат теста. А сам тест выглядит так:
@Test(dataProvider = "status-provider")
    public void testIsReplyAllowed(PrivateMessageStatus status, boolean expectedResult) {
        pm.setStatus(status);
        assertEquals(expectedResult, pm.isReplyAllowed());
    }

Почему так - ай-ай-ай:

  • Если упадет тест, то догадайся какой же случай все-таки свалился.

  • Дебажить такое тоже нелегко, тест-то всегда запускается со всеми параметрами, а что если нужен 3й?

  • Тест не показывает что же мы все-таки тестируем. Одно дело если тест называется userCanReplyToMessageInSentState - тут прям из названия понятна бизнес задача и что требуется от функции, а другое - это просто мапа Object-Boolean. Хотя, признаться, в этом случае все еще не так плохо.

Не мокайте то, что можно не мокать. Моканье сущностей, утилит - дело неблагодарное, не каждый вызов DOC’a нужно мокать. Самый яркий пример - моканье обычных сущностей (entities). Пример:

post = mock(Post.class);
when(post.getId()).thenReturn(POST_ID);
when(post.getUserCreated()).thenReturn(user);
when(post.getPostContent()).thenReturn(POST_CONTENT);
when(post.getTopic()).thenReturn(topic);

Почему бы не сделать: new Post(POST_CONTENT, topic).withId(POST_ID).withAuthor(user), ведь проще же, наглядней.

Избегайте использования полей в тестовых классах. Помните, что поля для класса глобальны и могут использоваться любым методом. Каждое добавленное поле в классе - существенное усложнение для чтения всех методов. Потому как для прочтения тестов часто не достаточно прочесть лишь метод, как правило используются разного рода моки, которые матрешкой собираются друг в друга. Чем меньше будет этого “шума”, тем проще будет читать каждый отдельный тест. И для новичков в вашем проекте намного проще будет писать свои тесты - для этого им не нужно будет читать 40 строк странных полей. Пример:

private static final String USER_NAME = "username";
    private static final String FIRST_NAME = "first name";
    private static final String LAST_NAME = "last name";
    private static final String EMAIL = "[email protected]";
    private static final String PASSWORD = "password";
    private static final String IMAGE_BYTE_ARRAY_IN_BASE_64_STRING = "it's dummy string";

    @Mock
    private UserService userService;
    @Mock
    private MessageSource messageSource;
    @Mock
    private ImageControllerUtils imageControllerUtils;
    private AvatarController avatarController;
    private byte[] validAvatar = new byte[] {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0,
            0, 0, 4, 0, 0, 0, 4, 1, 0, 0, 0, 0, -127, -118, -93, -45, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 1,
            -118, 0, 0, 1, -118, 1, 51, -105, 48, 88, 0, 0, 0, 32, 99, 72, 82, 77, 0, 0, 122, 37, 0, 0,
            -128, -125, 0, 0, -7, -1, 0, 0, -128, -23, 0, 0, 117, 48, 0, 0, -22, 96, 0, 0, 58, -104, 0, 0,
            23, 111, -110, 95, -59, 70, 0, 0, 0, 22, 73, 68, 65, 84, 120, -38, 98, -40, -49, -60, -64, -92,
            -64, -60, 0, 0, 0, 0, -1, -1, 3, 0, 5, -71, 0, -26, -35, -7, 32, 96, 0, 0, 0, 0, 73, 69, 78, 68,
            -82, 66, 96, -126
    };

Это не рекордсмен на моей практике, но уже такой перечень полей и констант должен отпугивать желающих его прочесть, а тем более, - дописать. Можно попытаться найти оправдания, мол, это позволяет избегать дублирования кода и уменьшить размеры самих тестов. Однако вы усложняете все(!) тесты для локального выигрыша. Если вам хочется подготовить данные или вы хотите избежать дублирования - используйте приватные методы, к примеру. Да, они тоже будут глобальны для класса, но они не являются обязательными для прочтения во всех тестах, для написания дополнительного теста вам не нужно перечитывать все методы.

Диагностировать такую патологию очень просто - если хоть один тест не использует ваше поле, начинайте думать. Если таких тестов становится больше, начинайте действовать.

Не используйте наследование. Разработчики любят изобретать. А еще любят писать красивый код. Жаль, что у них ни то, ни другое не получается. Тесты не исключение - частенько разработчики норовят написать очередной фреймворк для тестирования. Это случается когда у большого количества тестовых классов есть что-то общее. К примеру, у нас есть интеграционные тесты и все создают какой-то контекст и подготавливают какое-то окружение, а затем многие из них производят похожие действия для полной готовности SUT. И тут идея.. а не написать ли нам абстрактный тестовый класс, который будет это все делать, а мы его будем лишь наследовать в конкретных тестах. Даешь изобретателям по ушам! Проблема в том, что наследование привносит свою долю сложности в дизайн классов. Недаром от него в последнее время отказываются в пользу делегирования (гуглим delegation over inheritance). Так вот, имейте в виду, что для понимания вашего класса вам придется ити вверх по иерархии и читать предка. Вы ведь не знаете вот так наугад что в нем.

Следующая проблема возникает сразу после введения общего предка - ведь оказывается тесты группируются, и одной группе нужно меньше однотипных действий вполнять, другой - больше. И тут идея.. а не сделать еще один абстрактный класс, который наследуется от предыдущего абстрактного класса? Даешь изобретателям по рукам! Ведь теперь нам для чтения теста нужно лезть все глубже и глубже. А про эти супер-классы нужно еще знать. И они тоже обрастают логикой. И каждый из них имеет свои поля.

В общем был у меня опыт работы в проекте, где такая иерархия была доведена до абсурда. А все ведь намного проще - сделайте вы этот класс с общей логикой, да вызовайте разные его методы в @BeforeMethod. Да, в каждом тестовом классе это нужно делать, но это понятно, это явный вызов явного метода с нормальным названием.

Замечу, что я не противник всяких AbstractTransactionalTestNGSpringContextTests - во-первых, этот класс всем известен, не только тем, кто его писал. Во-вторых, хороший фреймворк - это фреймворк, в исходники которого не нужно заглядывать каждые 5 минут, в исходники этого класса вряд ли приходится заглядывать постоянно. Не у каждого рядового разработчика получается написать такой фреймворк.

Избегайте использования @Before. Не вообще, конечно, - создать объект SUT’a вполне там можно, и моки заинжектить тоже. Но случается что используют prerequisite методы слишком интенсивно. Пример:

@BeforeMethod
    public void init() throws NotFoundException {
        initMocks(this);

        Branch branch = new Branch("branch", "branch");
        branch.setId(BRANCH_ID);

        Topic topic = new Topic(user, "title");
        topic.setBranch(branch);
        topic.setId(TOPIC_ID);

        post = new Post(user, POST_CONTENT);
        post.setId(POST_ID);
        post.setTopic(topic);
        topic.getPosts().addAll(asList(post));

        when(postService.get(POST_ID)).thenReturn(post);
        when(topicFetchService.get(TOPIC_ID)).thenReturn(topic);
        when(breadcrumbBuilder.getForumBreadcrumb(topic)).thenReturn(new ArrayList<Breadcrumb>());

        controller = new PostController(
                postService, breadcrumbBuilder, topicFetchService, topicModificationService,
                bbCodeService, lastReadPostService, userService);
    }

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

Read more
12 Mar 2013

Интеграционные тесты + Maven

Резюме: хоть Maven и является инструментом, стандартизирующим структуру проекта и его сборку, однако он с крахом провалил стандартизацию разного рода тестов. Ща разберем полеты.

Итак, почему же все-таки Maven не справляется со стандартизацией тестов, ведь:

  • Модульные тесты:

    • Выполняются на фазе test
    • Лежат в src/test каталоге
    • Названия тестовых классов содержат слово Test
  • Интеграционные тесты:

    • Выполнаются на фазе integration-test
    • Лежат в src/test
    • В их имени должны быть буквы IT обозначающие собсно IntegrationTests.

Проблем на самом деле с этим несколько:

  • Фаза integration-test выполняется после модульных тестов, это значит что отдельно их не запустить. Каждый раз когда мы запускаем интеграционные тесты, выполняются модульные. Однако обычно мы хотим запустить модульные тесты один раз, а затем отдельно запускать интеграционные. Выходит чтоб модульные тесты не выполнялись нам нужно пропускать их с помощью -DskipTests, затем окажется что интеграционные тесты тоже не запускаются потому что failsafe плагин использует под собой surefire, начнется геморрой с созданием профилей и в конце концов начнет казаться что все это слишком сложно. Кстати, почему нам важно запускать тесты раздельно:

  • Разработчики могут быстро получить “зеленый” фидбек и продолжить работать. Именно модульные тесты способны быстро дать базовый ответ.

  • Немодульные тесты зависят от окружения, а значит могут падать. Если упали такие тесты, то еще нужно разобраться из-за окружения ли, либо и вправду сломалась логика.

  • Интеграционные тесты медленные, в зависимости от длительности их можно запускать по каждому коммиту, раз в день и т.п. Они тоже могут дробиться на более мелкие группы тестов, например, основные Smoke тесты выполняются первыми, затем регрессия, затем приемочные, затем какие-нибудь нагрузочные и т.п. Их удобно разделять потому что мы точно будем знать что сломалось. А также мы можем запускать отдельно определенную группу тестов и не ждать 4 часа чтоб прошло все.

  • Мы можем хотеть запускать одни и те же системные несколько раз. Например, мы их прогнали по текущему коммиту, а затем еще по тому что был неделю назад для того чтоб увидеть разницу между результатами.

  • Для системных тестов часто нужно подготовить окружение прежде чем их запускать.

  • Стандартно failsafe использует каталог src/test, а нам редко когда нужно помещать интеграционные и модульные тесты и ресурсы в одни и те же пакеты.

  • Фаза integration-test запускается перед install каждый раз. Мы же не хотим запускать медленные интеграционные тесты каждый раз когда устанавливаем артефакто в локальный репозиторий.

  • К сожалению для большинства разработчиков существуют лишь модульные, интеграционные тесты и “то, что делают QA”. Однако на самом деле тесты разделяются как минимум на модульные, системные и компонентные в зависимости от масштабов. Также есть функциональные и нефункциональные тесты и т.п. Более подробно можно с видами тестирования ознакомиться в одноименной статье. Но что главное - мы можем захотеть разделить все эти тесты. Для одних нужно поднимать все приложение, для других - лишь часть, для третьих вообще одного класса хватит. Однако Maven их никак не различает и не разделяет, у него есть либо модульные, либо интеграционные.

В общем подумав немного можно прити к выводу, что раз стандартный механизм Maven настолько несовершенен и все равно не сможет поддерживать всего нужного, мы можем отойти от него. Вместо этого предлагаю использовать plain old surefire plugin. Да, этот плагин заточен на написание модульных тестов, однако они по факту ничем не будут отличаться от “немодульных” - те же JUnit/TestNG будут описывать всю их логику (хотя тут позволяется также использовать всякие BDD фреймворки навроде JBehave, однако не о них речь).

Так вот как же это будет выглядеть. Для каждого из видов тестирования мы будем создавать а) профиль б) каталог с исходниками и ресурсами. Конфигурироваться же Maven будет следующим образом:

<properties>
    <test.sourceDirectory>${project.basedir}/src/test/java</test.sourceDirectory>
    <test.resourceDirectory>${project.basedir}/src/test/resources</test.resourceDirectory>
  </properties>

  <build>
    <testSourceDirectory>${test.sourceDirectory}</testSourceDirectory>
    <testResources>
      <testResource>
        <directory>${test.resourceDirectory}</directory>
      </testResource>
    </testResources>
  </build>
  <profiles>
    <profile>
      <id>component-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/component-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/component-test/resources</test.resourceDirectory>
      </properties>
    </profile>
    <profile>
      <id>system-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/system-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/system-test/resources</test.resourceDirectory>
      </properties>
    </profile>
  </profiles>

Заметьте, что в относительно небольшом куске конфигурации мы описали 3 вида тестов и все они находятся в разных каталогах:

  • Модульные: mvn test

  • Компонентные: mvn test -Pcomponent-test

  • Системные: mvn test -Psystem-test

Структура каталогов тогда такая:

src
 |_main
      |_java
      |_resources
 |_test
      |_java
      |_resources
 |_component-test
      |_java
      |_resources
 |_system-test
      |_java
      |_resources

Правда системные тесты как правило имеет смысл выделять в отдельные модули, ато и проекты.

Единственное в чем я слукавил: фаза интеграционных тестов делиться на несколько шагов и это позволяет нам сначала поднять окружение, а затем его остановить. Подход с surefire такого не даст, вам придется вручную запускать окружение с помощью явно указанных команд. Однако:

  • Если нам нужно полностью развернутое окружение, то это не всегда возможно автоматизировать без написания скриптов, что значит что в большинстве случаев все равно придется руками дергать их

  • Сложность в конфигурировании разного рода тестов перекрывает сложность запуска какого-нибудь tomcat:start

  • Т.к. обычно системные тесты выносят в отдельные модули или проекты, то там вполне можно использовать фазу integration-test как единственную, которая запускает тесты.

Но если вам правда хочется удобным образом автоматизировать развертывание окружения, возможно вам будет лучше сконфигурировать integration-test фазу для этого.

JUnit Categories, TestNG Groups

Последнее о чем стоит упомянуть - это JUnit категории и TestNG группы. Они позволяют с помощью аннотаций как-то помечать тесты. Однако если в TestNG это хоть как-то можно сделать удобно, то в JUnit это сделано через прямую кишку и вам придется конфигурить много профилей, у вас не выйдет задать exclude=IntegrationTest по умолчанию, а затем через командную строку активировать какие-то другие категории, для этого вам придется все равно создавать профили, а значит никакого удобства такой способ не принесет. Раз не выйдет это использовать с JUnit’ом, значит не имеет смысл стандартизировать такой подход.

Read more
18 Dec 2012

Виды тестирования (Types of testing)

Сначала имеет смысл заметить, что многие вещи разные люди именуют по-разному, я опишу термины, которыми пользуюсь сам, а также слышу чаще всего при общении с коллегами, ну или нахожу в интернетах.

  • Модульные тесты. Их задача - протестировать отдельный модуль логики в полной изоляции - как от окружения, так и от других модулей. Это не обязательно должен быть один класс, это может быть их небольшая агломерация - например, в случае с одним главным классом и с несколькими вспомогательными package-private классами.
    Для того, чтоб отделить тестируемый модуль (SUT, system under test) от других модулей (DOC, dependent-on class) и от внешней среды, обычно приходят на помощь mock-фреймворки, которые подменяют все остальное на манекены-обманки (перечень библиотек можно найти внизу). Например, если у нас есть парсер HTML страничек, нам не обязательно для него лезть на реальный сайт, качать страницу и отдавать, мы можем сохранить где-то эту страницу в тестах и постоянно использовать во время тестирования. В таком случае класс доступа к внешнему ресурсу мокается и возвращает нужный контент когда его просит SUT.
    Почему они важны? Потому что они могут протестировать все - каждое условие в классе, каждый цикл, каждый метод. Таких тестов как правило больше всего.
    Существует некая классификация описанная в XUnit Test Patterns (краткое пояснение можно найти в блоге Фаулера). Мое личное мнение - эти названия высосаны из пальца и использовать их не стоит. Терминология в тестировании и так путает, а тут еще появляются Dummy, Fake, Mock, которые слишком близки по семантическому смыслу, чтоб их серьезно рассматривать в качестве инструмента для общения. Да и уровень модульного тестирования и так слишком низок, чтоб моки нужно было делить на типы. На моей практике эти термины нужны были только во время собеседований.

  • Компонентные тесты. Термин используется не так часто, однако мне очень понравился. В данном случае имеется в виду компонент системы. Для этого не нужно запускать все приложение. Обычно в наших приложениях мы делим все на слои. Такие компонентые тесты могут инициализировать все слои, мокать внешние ресурсы, и тестировать целый кусок логики. Плюсы таких тестов: а) тестируют не класс, а настоящую предметную логику б) очень быстрые по сравнению с системными тестами. Идеальные компонентные тесты - те, в которых все внешние ресурсы ограничены, однако иногда проще что-то оставить, например, если это flow использующее JMS внутри единого приложения, то сам JMS можно оставить (заменив его на ActiveMQ например), а вот обращение к внешним системам уже придется мокать, иначе мы будем тестировать и их, и в таком случае слишком много времени будет тратиться на поддержку.
    Бывает компонентными тестами называют модульные. Ну и по правде говоря компонентом вы можете назвать что угодно, так что аккуратно с этим термином.

  • Интеграционные тесты (System Integration Test, SIT). Этот термин самый перегруженный из всех, интеграционным тест могут назвать в случаях: когда просто используется внешний ресурс; когда используется агломерация классов (в нашем случае это Компонентные); когда запускается все приложение и т.п. Дайте людям повод, они назовут этот тест интеграционным. Я стараюсь не использовать по возможности этот термин, однако бывают семантически подходящие случаи: это когда вы на самом деле тестируете интеграцию с другими системами. Пример: приложение А следит за обновлениями в приложении Б и дергает приложение С. Все эти системы могут общаться по разным протоколам: JAX-RS, JAX-WS, JMS, Protobuf, нам нужно протестировать это общение потому как протокол с одной стороны мог поменяться в то время как с другой стороны он остался прежним, и тогда приложение в целом не работоспособно. Собственно этим и занимаются интеграционные тесты, они тестируют точки соприкосновения систем. Заметьте, что они не должны тестировать саму бизнес логику, иначе вы, опять же, будете тестировать несколько приложений, а вам этот геморрой не нужен.

  • Системные тесты. Это тесты, иже нуждаются в запущенном приложении. Их можно как автоматизировать, так и предоставлять работу ручным тестеровщикам. Проблема этих тестов: их трудно писать и автоматизировать потому как все окружение должно быть поднято; они медленные; их трудно поддерживать, потому как любой чих может их сломать. Таких тестов должно быть мало по сравнению с модульными и компонентными, они должны тестировать основные функции приложения и не вдаваться в детали.
    Эти тесты должны быть максимально независимыми друг от друга. Например, если у вас есть сайт где можно зарегистрироваться, то в идеале каждый тест должен регистрировать нового пользователя, это важно для распараллеливания их выполнения когда окажется что они проходят полдня.


Еще несколько терминов, хоть и ортогональных вышеприведенным (потому как они одновременно могут быть и системными например), однако очень используемых:

  • Sanity тесты - базовые системные тесты, которые определяют а работоспособно ли приложение вообще. Удобно использовать при каждом деплое дабы автоматизированно определить запустилось ли приложение удачно. Включает в себя проверку только самых-самых базовых функций системы. Вот один из примеров реализации написанный на питоне - он просто опрашивает стартовую страницу, если та отвечает HTTP 200/201, значит приложение стартовало, если же там коды ошибок, значит доставка сайта упала и сборку в Jenkins’e нужно помечать красным.

  • Smoke тесты - судя по классификации ISTQB - это то же самое, что и Sanity. Однако многие выделяют их в разные категории. Бывает говорят, что Smoke - это регрессионное тестирование главных функций дабы определить имеет ли смысл дальше тестировать или приложение уже серьезно повреждено (начинает дымиться), в то время как Sanity говорит работает ли приложение вообще. Другие трактуют эти термины в точности наоборот (хотя семантически приведенный вариант больше похож на правду). В своей команде вы просто определитесь сами что вы как называете и имейте в виду что вас могут неверно понять.

  • Story Acceptance Tests. Когда вы сформировали требования, вы можете определить тесты (и иногда даже сразу их написать), которые будут определять удовлетворяет ли функционал заведенным требованиям. Очень круто если заказчик их сам пишет, однако такая возможность редка. Если такие тесты не проходят, значит фича не является реализованной. Очень удобно такие тесты писать в виде Given some state, when doing something new, then we find a new state.

  • User Acceptance Tests - это когда вы садите настоящих пользователей (даете доступ заказчику) и просите покликать, ожидая какую-то обратную связь. Это тот этап, когда вы можете определить а то ли вообще хотел заказчик.

  • Регрессионные тесты (Regression Tests). Ваши приемочные тесты, после того как отслужили свое, и ваша фича сдана заказчику, смогут продолжать служить в форме регрессионных тестов. Такие тесты определяют не сломался ли старый функционал во время реализации новых фич или после рефакторинга. Важность этих тестов нельзя преувеличить. Если вы их автоматизировали, вы сможете делать частые релизы. Не имея же автоматизированной регрессии, вы будете вынуждены перетестировать многое вручную перед каждым релизом. Автоматизированная регрессия позволяет вам чувствовать себя уверенным человеком :)

  • Нагрузочные тесты. Определяют как ваше приложение ведет себя под нагрузкой и собственно какую нагрузку они могут выдержать. Достаточно сложные тесты, особенно учитывая, что сама нагрузка - вещь многоликая. Можно тестировать пропускную способность (напр., количество транзакций в секунду), максимальную нагрузку (когда приложение уходит в себя насовсем), однако чаще всего нас будет интересовать capacity (дам конфетку тому кто переведет это на русский :)) - количество одновременных транзакций при которых приложение будет себя вести удовлетворительно (для этого вы ставите изначально порог, к примеру, каждая страница не должна загружаться дольше чем 1 сек).

  • Тесты на выносливость (Longevity tests) - это подтип нагрузочных тестов. Отличается тем, что вы просто помещаете ваше приложение под постоянную среднюю нагрузку эдак на месяц и не перезагружаете его. Такие тесты помогают определить, например, есть ли у вас утечки памяти.


Обзор библиотек для тестирования:

  • JUnit, TestNG для запуска тестов и проверки результатов. Очень понравился Groovy JUnit, о нем можно послушать на одной из JTalks Тех Сессий.

  • Mockito, EasyMock - самые популярные библиотеки для создания моков

  • PowerMock - позволяет подменять static, final, private методы, такая библиотека помогает в legacy системах, трудно поддающихся рефакторингу

  • Awaitility - позволяет удобно работать в случаях, когда нужно ждать ответ некоторое время. Сразу оговорюсь, что это очень не эффективно, намного эффективней использовать какие-то callback’и.

  • Rest Assured, Restito - для написания тестов, работающих с REST API. Первый может тестировать REST сервер, то бишь слать запросы вашему сервису; а второй может запускать mock-сервис и тестировать как ваш REST клиент шлет запросы другим системам.

  • JBehave, Cucumber (Ruby) - фреймворки позволяющие описывать тесты в виде Given/When/Then (так называемые Specification By Example). По опыту с первым скажу, что вещь на практике не такая удобная, более подробно в Рецензии на JBehave.

Read more