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
30 Apr 2014

A collection was no longer referenced by the owner

Полная ошибка выглядит так:

org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" 
was no longer referenced by the owning entity instance: 
org.javatalks.training.hibernate.entity.bag.Book.comments`.

Собственно ошибка говорит о том, что была у Сущности коллекция, стоял значит у нее all-delete-orphan, а тут вместо этой коллекции Сущности подсунули другую. Код, ответственный за такой случай находится в классе org.hibernate.engine.internal.Collections:
if ( e != null && e.getStatus() != Status.DELETED && e.getStatus() != Status.GONE ) {
	throw new HibernateException(
			"A collection with cascade=\"all-delete-orphan\" was no longer referenced by the owning entity instance: " +
			loadedPersister.getRole()
	);
}

Разработчики вполне намеренно проверяют, если на коллекцию прежний владелец не ссылается (и владелец не был удален), то это проблема. Почему это проблема к сожалению никто не указывает - видимо это перестраховка на случай когда пользователь случайно перезаписал коллекцию. Правильно все-таки делать remove()/removeAll() на самой коллекции. Два случая когда такое воспроизводится (код на Groovy):

Коллекция перезаписывается и владелец update’ится

@Test
    void 'when updating, if orphan collection is dereferenced, exception thrown'() {
        List<Comment> comments = [new Comment(body: "comment1"), new Comment(body: "comment2")]
        Book original = new Book(title: "with comments", comments: comments)
        bookDao.save(original).flushSession()

        try {
            //it's not allowed to replace collection with cascade=orphan
            original.comments = [new Comment(body: 'new')]
            bookDao.saveOrUpdate(original).flushSession()
            assert false
        } catch (HibernateException e) {
            assert e.message.startsWith('A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance')
        }
    }

Владелец merge’ится, однако новая коллекция - null

А вот этот случай хитрый. Обычно если мы merge’им детачнутую сущность, то коллекция нормально обновляется - отсутствующие в новой коллекции элементы будут удаляться. Но не тут-то было когда коллекция является null’ом:

@Test
    void 'when merging, if new collection is null, exception thrown'() {
        List<Comment> comments = [new Comment(body: "a"), new Comment(body: "b")]
        Book original = new Book(title: "with comments", comments: comments)
        bookDao.save(original).flushAndClearSession()

        try {
            bookDao.merge(new Book(id: original.id, comments: null))//exception only due to null
            bookDao.flushSession()
            assert false
        } catch (HibernateException e) {
            assert e.message.startsWith('A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance')
        }
    }

Read more
13 Jan 2014

Nexus - почему не нужно использовать LATEST

Везде, где мне приходилось работать с Sonatype Nexus в какой-то момент возникало недопонимание о том что такое LATEST версия артефакта и почему ее нельзя использовать.

Любой артефакт обладает неким информационным файлом maven-metadata.xml, содержимое которого приблизительно такое:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <versioning>
    <latest>2.9.1</latest>
    <release>2.9.1</release>
    <versions>
      <version>2.8.1</version>
      <version>2.9</version>
      <version>2.9.1</version>
    </versions>
    <lastUpdated>20130628064237</lastUpdated>
  </versioning>
</metadata>

Итак, вы захотели получить последнюю версию артефакта. Вы быстро нагуглите как это можно сделать: http://somerepo.org/service/local/artifact/maven/redirect?r=central-proxy&g=groupId&a=artifactId&v=LATEST И первое время это возможно даже будет работать. Однако в какой-то момент Nexus начнет вам возвращать старые артефакты.

Это происходит из-за алгоритма, который использует Nexus для нахождения LATEST: сначала смотрит если в maven-metadata.xml есть <latest> и возвращает его. Если тега нет, то вернется последняя версия в списке <versions>.

Изначально механизм этот работает лишь потому, что в maven-metadata.xml просто нет <latest>. Версии отсортированы в порядке добавления, возвращается последняя. Но в какой-то момент происходит что-то из двух:

  • Кто-то таки проставляет <latest> в мета-файл.

  • Работа идет в ветках и две команды независимо друг от друга заливают артефакты с разными версиями. Кто залил последний - того и последняя версия. Просто-напросто в конец добавляется та версия, которая сейчас деплоится.

Как оказывается в Nexus LATEST работает правильно только для плагинов и использовать этот механизм для обычных артефактов нельзя.

Несколько ожидаемых вопросов:

Кто обновляет maven-metadata.xml?

Этот файл обновляется не Nexus’ом, а Maven Deploy Plugin’ом: сначала он забирает из репозитория существующий maven-medatadata.xml, затем обновляет информацию и заливает обратно в репозиторий. Если существующего файла нет, создается новый и заливается в Nexus.

Откуда берется <latest> если Maven его никак не использует?

Maven этот тег и не проставляет. У Nexus’a есть некие утилитарные механизмы, среди таких - Rebuild Metadata. Этот пункт можно найти в контекстном меню репозиториев и каталогов. Этот механизм удаляет существующую maven-metadata.xml, просматривает артефакты и создает новый файл. И в этот момент он создает <latest>. Для чего - хрен знает, но создает. Причем я замечал <latest> даже в том случае, когда Rebuild Metadata вроде как и не делался, поэтому есть подозрения что это происходит еще по каким-то событиям. Имейте в виду, что сортировка версий происходит исходя из механизмов зашитых в Maven, а эти механизмы уже используются Nexus’ом:

The ordering is derived by parsing the version string and supports sematic versioning with additional semantics for specific classifiers. Further details can be found in the documentation for the implementing class GenericVersionScheme.

Почему <latest> устаревает?

Даже если Rebuild Metadata и происходит, то это разовое действие. Дальше метадата-файл обновлять все равно будет Maven. А тот просто ничего не делает с <latest> - она копируется из того maven-metadata.xml, который был загружен из Nexus’a.

Подведем итоги

Если вам нужен механизм получения последней версии артефакта, этот механизм вы сами и должны будете создать (написать скрипт). Механизм, используемый Nexus’ом по умолчанию в какой-то момент просто перестанет работать.

К тому же если вы считаете вам нужна где-то последняя версия артефактов, скорей всего вы двигаетесь в неправильном направлении, мне не приходилось видеть хорошо поставленные процессы где была нужда в LATEST.

PS: ссылки на Sonatype Wiki сломались, они либо перенесли куда-то страницы, либо убрали их из открытого доступа. Я обновлю адреса как только найду новое расположение.

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
07 Nov 2013

Рецензия на ZK Framework

Резюме: после некоторого использования ZK Framework он оказался хорошим решением для очень простых приложений, однако в реальной жизни проявились его неприятные особенности (здесь я приведу опыт 2012 года, к текущему времени могло что-то поменяться). В общем отрицательных впечатлений намного больше, рекомандовать этот фреймворк не могу.

Отрицательные стороны

Ужасное community, такое же как и код. Платную поддержку не использовал и сказать ничего о ней не могу, скажу о бесплатной. Форум у ZK присутствует, и даже ответы какие-то есть. Однако из тех вопросов, которые я там задавал (а их было штук 10) почти ни на один отвечено не было. Это не такая большая проблема если исходники на руках, однако в них такой г-код, что понять как что работает очень сложно. К тому же все построено на событиях и есть некий Event Bus, что еще более усложняет понимание внутреннего исходного кода.

Также мало кто использует этот фреймворк по сравнению с теми же JSF, Spring MVC, поэтому помощи обычно ждать не откуда.

MVVM sucks. MVVM (Model-View-ViewModel) - это подход, когда у страниц есть маппинг к определенному объекту и при обновлении состояния Вида обновляется и объект. Ну и наоборот. В демках выглядит все просто прекрасно (собсно поэтому и решился использовать этот MVVM), вот Вид:

<textbox value="@load(vm.person.name) @save(vm.person.name, before='save')"/>

Тот сохраняет значение поля в vm.person.name, оттуда же и берет значение для заполнения UI. Все круто пока не сталкиваешься с реальной жизнью, когда UI становится чуть сложней чем элементарный. Есть несколько проблем у этого подхода:

  • Он использует состояние в памяти. В отличие от MVC, который обычно просто сохраняет данные на каждый запрос в БД, MVVM как правило хранит все в памяти. От этого вы вскоре достаточно быстро начнете испытывать проблемы, потому как состояния хранить нужно будет все больше.

  • В сложных (да даже обычных) UI обычным binding’ом не обойтись - нужно какой-то pre/post processing, нужно обновлять 10 других мест. Да, это все возможно и здесь, только опять же, потому что изменять нужно не одно место (хранилище данных), а много (разные вьюхи, которые в памяти храняться, то же хранилище данных), это становится задачей несколько нетривиальной.

Более детальные негативные отзывы вы можете почитать в блогах .Net разработчиков (которые тоже в свое время пропагандировали этот шаблон).

На самом деле ZK поддерживает как MVVM, так и MVC с MVP, однако больше всего они счас занимаются именно MVVM и его PR’ят. Собственно не ведитесь на удочку.

ZK позволяет разрабатывать только простой UI. Вообще ZK себя зарекомендовал как фреймворк для админок. Потому что там особо не надо выпендриваться со сложным и красивым UI. Простые элементы - это конечно хорошо, но даже для админок иногда приходиться сделать что-то более сложное, чем combobox. В такие моменты приходится разбираться не только с JavaScript’ом, но и с тем как писать компоненты для ZK.

Дружелюбность SEO. ZK приложения как правило одностраничны и под это заточен фреймворк. Проблема в том, что такие приложения либо не индексируются вовсе, либо индексируются плохо. Особенно учитывая что HTML у ZK выходит до визга сложный. У Zkoss есть некоторые наработки в этом направлении, однако в их эффективность верится мало.

Положительные стороны

Никакой компиляции Java->JS. В отличие от GWT/Vaadin, ZK framework не генерирует никаких JS файлов из Java классов, что не замедляет разработку таких приложений.

Быстрый UI. Если посмотреть на тот же Vaadin, то создается впечатление заторможенности. Он достаточно медленно реагирует на мышь. А вот что мне понравилось в ZK - так это быстрое время отклика.

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
09 May 2013

Deployment Pipeline на практике

Резюме: в сети появляется много теоретического материала по теме Continuous Delivery & Deployment Pipeline’ов, однако практических реализаций в примерах пока не находил. Собственно здесь хочу поделиться примером того как мы реализовали Deployment Pipeline внутри нашего open source проекта JTalks. Все исходники открыты, поэтому вы сможете не только прочесть пояснение, но и своровать реализацию.

Если вы не понимаете что такое Deployment Pipeline и зачем он нужен, вот некоторые пункты, однако для полного понимания нужно прочесть до конца:

  • Deployment Pipeline - это набор практик для автоматизации развертывания приложений на разные окружения, включая production

  • Deployment Pipeline позволяет сделать релизы частыми и уменьшить риски провалов

  • Deployment Pipeline дает возможность ускорить работу разных команд (Dev, QA, DevOps) автоматизируя рутинную работу

Выглядеть это может к примеру так

Jenkins

Первое и центральное место - это собсно наш Continuous Integration сервер. Я рекомендую использовать именно Jenkins,
потому как он имеет сотни удобных плагинов, и половины которых вам не дадут никакие коммерческие инструменты типа
Team City, Bamboo. Дополнительный пункт в пользу Jenkins - он открытый и вы сами можете дописывать его плагины. Такая
возможность обычно приходит очень кстати в особо крупных проектах, где стандартные фичи не подходят и нужно много
писать своих решений.

Какие плагины Jenkins’a стоит рассматривать для реализации Deployment Pipeline’ов:

  • Build Pipeline Plugin - собсно. Этот плагин далеко не идеален как и большинство Jenkins плагинов, с некоторым количеством багов (которые вы можете исправить). Он предоставляет возможность визуализации вашего конвеера. Визуализация очень важна чтоб все члены команды включая будущих новичков быстро сообразили какие существуют окружения и как приложение эволюционирует. Также этот плагин контролирует возможность запускать те или иные планы. Для реализации настоящего Deployment Pipeline’a вам важно, чтоб ваши артефакты не переходили в следующие фазы до того как выполнились предыдущие. Например, ваше приложение не должно проходить на UAT или PREPROD окружения до того как прошли автоматизированные системные тесты.
  • Rebuild Plugin - например вы решили зарелизить ваше приложение, а уже в PROD окружении оказалось что что-то не так. На моей практике было такое, что приложение работало недопустимо медленно на реальных данных. Тогда у вас должна быть возможность откатить ваше изменение. Откат же не должен в идеале отличаться от обычного релиза, и если вам посчастливилось оказаться в такой ситуации - просто зарелизьте предыдущую версию вашего приложения! Rebuild Plugin позволяет перезапускать предыдущие билды с теми же параметрами.
    Конечно такая простая ситуация бывает не всегда - иногда вам нужно откатить изменения в базе данных. Тогда вам конечно еще придется восстанавливать бекапы. Сделайте так чтоб любое ваше окружение использовало все те же скрипты - тогда вы их оттестируете сотни раз еще на фазе разработки.
  • SCM Sync Configuration Plugin - это не относится напрямую к вашему конвееру, однако лучше версионировать изменения в вашем Continuous Integration сервере. Ибо чем дальше в лес, тем больше дров - CI в какой-то момент становится вашим самым важным и центральным инструментом со сложной конфигурацией. Этот плагин стоит использовать с большой осторожностью, потому как бывало что он просто отказывал. В последнее время мы начали задумываться о ручном бекапе.

Артефакты (Binaries)

Следующая остановка - хранилище ваших артефактов. Одна из главных особенностей настоящих Deployment Pipeline’ов - ваши артефакты собираются лишь один раз и используются на каждом окружении. Это важно по нескольким причинам:

  • Вы получаете повторяемость окружений. Нет даже такой возможности что на разные окружения попали разные исходники. Если же вы собираете каждый раз новые артефакты, то так утверждать уже не можете. Во-первых, потому что во время сборок могут использоваться какие-то параметры окружения, во-вторых сами инструменты сборки могут выпендриться. Например, Maven может использовать другую версию артефакта если вы используете version ranges: <version>[1.5-1.7)</version>.
    Другой пример - это сами Maven Repos (Nexus, к примеру), которые могут возвращать разные артефакты в зависимости от их настроек и описания pom.xml. Это может произойти если используются Repository Groups которые ссылаются на несколько репозиториев с одними и теми же артефактами.

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

  • Другие команды (QA) могут сами откатывать версии и таким образом локализовывать версию где был введен баг.

  • Ваш CI работает быстрей т.к. не нужно делать дополнительных сборок.

Хранить артефакты можно где угодно, даже тот же Jenkins позволяет их сохранять у себя и использовать Copy Artifact Plugin для передачи артефактов из плана в план. Однако намного удобней работать со специальными инструментами, такими как Sonatype Nexus, Artifactory. Есть несколько причин для этого:

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

  • Вам все равно они понадобятся для работы с Maven, Gradle - чтоб загружать из них зависимости.

  • Хранение артефактов в Jenkins’e усложняет с ним работу, например, нужно задумываться как делать бекапы эффективней.

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

Например, у нас в JTalks план сборки выглядит так:

#get rid of SNAPSHOT and add build number after version
VERSION=`mvn help:evaluate -Dexpression=project.version | grep -v "^\["| grep -v Download`
VERSION=${VERSION/%-SNAPSHOT/} #get rid of -SNAPSHOT if it's there
VERSION=$VERSION'.'$PIPELINE_NUMBER #add unique build number
mvn versions:set -DnewVersion=$version #update the versions in pom files onto unique ones
mvn clean package #run actual build and unit tests

У каждого плана в Jenkins есть специальные переменные, такие как BUILD_NUMBER (мы его переименовывали в PIPELINE_NUMBER) - такая переменная используется для уникальности, то бишь каждый артефакт получает соответствующий суффикс. Далее с этим суффиксом мы заливаем артефакт в Nexus в специальный репозиторий. Т.к. каждая сборка имеет свой уникальный номер-суффикс - мы этот номер передаем в другие планы нашего Deployment Pipeline’a, они отыскивают нужный артефакт и развертывают его на нужных окружениях.

Конфигурирование проектов

Для того, чтоб реализовать красивый и простой Deployment Pipeline, разработчики должны сделать некоторые усилия. Например, у вас ничего не получится если вы конфигурируете проект на этапе сборки. А такие есть (упаси!) - собирают по артефакту на каждое окружение, где разница лишь в конфигурации, а код - тот же. Чтоб все получилось вам нужно придерживаться одного правила: конфигурировать сборку нужно снаружи! Вы можете оставить некоторые окружения (включая общие локальные) внутри атрефактов и позволять переключаться между ними с помощью каких-то флагов: -Denvironment=UAT, однако также должна быть возможность задавать параметры снаружи. Это а) позволит разворачивать артефакт на любом окружении б) даст возможность хранить секретную конфигурацию (например, пароли от PROD базы) в отдельном репозитории без общего доступа.

Теперь о практике: в JTalks мы это реализовали расширив возможности Sping’ового PropertyPlaceholderConfigurer своим JndiAwarePropertyPlaceholderConfigurer. Идея в том, что сначала мы проверяем есть ли переменная в JNDI, и лишь потом, если не обнаружилось, смотрим в переменные среды и properties файлы внутри проекта. А определение какой-то опции может выглядеть так:

<bean class="org.jtalks.jcommune.model.utils.JndiAwarePropertyPlaceholderConfigurer">
  <property name="location" value="classpath:/org/jtalks/jcommune/model/datasource.properties"/>
</bean>
<bean ..>
  <property name="user" value="${JCOMMUNE_DB_USER:root}"/>
</bean>

Теперь как же задавать свойства снаружи? Наверно, у каждого AppServer’a есть JNDI, Tomcat не исключение. Создаем файл и кладем его в conf/Catalina/localhost/[app-namme].xml. А там пишем:
<?xml version='1.0' encoding='utf-8'?>
<Context>
  <WatchedResource>WEB-INF/web.xml</WatchedResource>
  <Environment name="JCOMMUNE_DB_USER" value="root" type="java.lang.String"/>
</Context>

Все, параметр задан в JNDI и если он там указан, то будет браться именно оттуда. Если же его там нет, то смотрим в env vars или properties файлы.

Инструмент сборки

Каждый проект конечно должен собираться специальным инструментом. Есть всякие Ant, Maven, Gradle. Здесь рассмотрим последние два. Maven к сожалению не вписывается в общую картину. Наша идея - иметь уникальный артефакт после каждого коммита, однако Maven прописывает свои версии в pom.xml и менять их после каждого коммита как-то не комильфо. Я такой вариант видел в крупных проектах когда использовались тематические ветки + валидация проходила на этапе прекоммита, при этом версия обновлялась только когда код попадал в develop ветку как это любят изображать Git фетишисты. Однако для мелких и средних проектов - это излишество, да и на крупных не факт что хороший вариант. Короче говоря мы выкрутились в JTalks тем как раз, что подменяли версию, заливали артефакт, но коммит изменений в pom файлы мы не делали.

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

Scripting Language

Для автоматизации релизов вам придется писать скрипты. Это может быть bash, python, ruby, groovy, вы вольны выбирать сами. Bash’a вам вряд ли хватит, а все остальные особо друг от друга не отличаются. Однако есть преимущества каждого из них:

  • Python установлен на большинстве Linux дистрибутивов, дополнительно не придется ставить никаких платформ.

  • Плюсы же Ruby в том, что затем нам могут понадобится инструменты для настройки окружений такие как Chef, которые используют Ruby. Вы даже можете собственно Pipeline реализовать на Chef. Однако все сервера должны быть с предустановленным soft’ом для этого.

  • Groovy удобен тем, что в качестве инструмента сборки можно использовать Gradle и тогда включить скрипты в сами исходники проекта.

Свои скрипты стоит пакетировать, например, Python имеет свой package manager - PIP. Это удобно потому как вы можете версионировать артефакты самих скриптов, да и установка будет намного более тривиальной.

Пример таких скриптов на Python вы можете посмотреть в наших открытых исходниках.

Виртуализация

Для того чтоб все окружения были еще более похожими и не возникало неожиданных ситуаций связанных с операционной системой либо железом, стоит задуматься о виртуализации ваших окружений, вплоть до виртуализации PROD’a. Это вам также даст прирост в производительности труда т.к. избавит от ручной настройки новых окружений. Дополнительным плюсом может быть то, что в определенный момент вы захотите использовать облачные решения типа Amazon EC2, тогда переход на них будет еще более прост.

Есть еще такой прекрасный инструмент как Vagrant, который сильно облегчает проблемы с повторяемыми окружениями. Вам лишь нужно описать какой образ использовать, какой софт будет установлен (это делается благодаря интеграции с Chef и Puppet) и ву а ля - готовое окружение в одну команду. Теперь даже самые малотехнические люди могут развернуть вашу систему без проблем. JTalks VM демонстрирует возможности Vagrant’a:

  • Устанавливаете Vagrant, качаете сами Vagrant скрипты из гит приведенного выше репозитория

  • vagrant up - и через некоторое время, когда все закачается и установится, вы получаете дистрибутив Ubuntu с установленными там deployment скриптами проекта, а также MySQL, Tomcat’ом.

  • Заходите на виртуалку vagrant ssh и запускаете один из проектов: jtalks deploy --environment vagrant --project jcommune --build 2280. Видите последнюю цифру? Она как раз и значит номер того артефакта, который мы разворачиваем. Еще раз ссылка на артефакты.

  • Ну и открываете браузер, вот вам полностью установленное в несколько кликов приложение: http://localhost:4444/jcommune

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
23 Jan 2013

Как проводить собеседования?

Расскажу свою точку зрения о том как проводить собеседования на должность разработчика, в частности на Java разработчика.

В первую очередь расставим цели:

  • Подобрать разработчика в свою команду

  • Он должен помочь нам с проектом технически

  • С ним должно быть приятно работать

К сожалению большинство собеседующих забывают об этих целях и во многих случаях собеседование скатывается к:

  • Озадачить кандидата задав ему сложный вопрос

  • Показать как я крут

  • “Он решил эту задачу, берем!” или “Он не решил эту задачу, он не достоин”

Как же все-таки должно проходить собеседование:

  • Для начала вам нужно расслабить кандидата (сделайте ему массаж ;)). Человек, отвечающий на вопросы и человек, работающий в команде, - это два разных человека. Хорошего разработчика можно запутать и он будет плохо отвечать на вопросы, и наоборот - плохой разработчик может знать как все должно выглядеть в теории, но никогда не делать этого на практике. Вам нужно добраться до разработчика спрятавшегося на время собеседования где-то глубоко внутри, а не послушать ответы “студента”.
    Если в его резюме перечислены хобби, поспрашивайте о них. Если для вас важно, чтоб человек знал английский, начните с проверки английского, а там уже можно порасспрашивать и про увлечения, и про школу с дет садиком, таким образом убьете двух зайцев. Важно, чтоб человек с вами начал общаться как с коллегой, но и в кабак его при этом вести не стоит - всему есть меры.
  • Не используйте тесты. Никогда вам тесты не покажут ни квалификацию разработчика, ни того какой он командный игрок. Во-первых, тесты просто раздражают, они нудные. В итоге разработчик может уйти от вас с негативом, а это в свою очередь скажется на авторитете компании, он ведь обязательно поделится своими впечатлениями с друзьями. Во-вторых, вы думаете внешние (центральные) экзамены, которые школьники сдают для поступления в ВУЗ хоть как-то показывают уровень студентов? Э-э-э нет, уровень - это не знание ответа на вопрос, а понимание предмета.

  • Попросите кандидата показать свой код прежде чем приглашать на собеседование. У многих есть какие-то open source проекты, которыми им будет приятно похвастаться.

  • Дайте человеку пописать код (особенно если у него не было чем похвастаться перед собеседованием). Прям на собеседовании выделите ему машину и IDE, чтоб увидеть как этот человек пишет. Причем,

    • Условия должны быть максимально приближенные к реальным. Человек не должен писать в notepad, это глупо, он не будет писать так на работе.
    • Задание не должно быть сложным, любое сложное задание может занять дни, даже если вы считаете что оно короткое и его можно написать за полчаса, у кандидата может сложится другая картина. Например, вы можете предложить написать какую-то коллекцию. Никогда вам человек не напишет правда качественную, оптимизированную коллекцию, с документацией и тестами за полчаса. Если же вам нужны лишь очертания коллекции, тогда объясните четко кандидату что вы от него хотите. Имейте в виду, что вам важней то, как кандидат пишет в реальной жизни, а не то как он пишет “когда надо быстро-быстро ой-ой-ой”. Я предпочитаю давать задачи, типа, найти максимальное число. Она тривиальна, поэтому кандидат вполне может уложиться в полчаса. Также из очень классных заданий - написание immutable классов, это элементарно сделать и это важно в каждодневной разработке.
    • Кандидат должен четко понимать что от него хотят. Если вы хотите оптимизированное решение, объясните это кандидату. Если вам нужно близкое к реалиям решение, объясните это кандидату. Если вам важно просто понимание кандидата, объясн…
  • Старайтесь не спорить с кандидатом, ваша цель - определить его уровень, а не переспорить его.

  • Задавайте вопросы по-настоящему важные для вас. Если у вас в проекте используется технология А, Б, В, а вы для комплекта расспрашиваете о Э, Ю, Я которые и близко к вам не относятся, значит вы не преследуете свою изначальную цель - найти сотрудника в ваш проект.
    Бывает, что компания крупная и важно знать уровень разработчика в разных областях, мол, может другой проект возьмет. В таком случае сначала убедитесь, что он не подходит вам, ну и все равно знайте меру.

  • Вам нужен не ответ, а понимание предмета. Вы не должны останавливаться на том, что человек дал вам ответ на конкретно поставленный вопрос, вам нужно обязательно копнуть глубже, попросить привести разные примеры, как человек то или иное применял на практике и т.п. Просто ответ вас не интересует, вас интересует насколько глубоко человек разбирается в предмете. С одной стороны кандидат который разбирается в предмете может не знать определенного ответа но разбираться в общем; ну и как всегда есть те, кто просто знает ответы на вопросы.

  • Неправильный ответ на заковыристый вопрос не дает минусов кандидату. Если вы задаете очень тонкий вопрос, вас не должно смущать что человек этого не знает. Во-первых, какой прок от этого вопроса? Что он, определит уровень кандидата? Эти знания, возможно, в жизни ему никогда не пригодятся. Во-вторых, ну не наткнулся он на ту же статью, что вы прочли вчера, зато он может быть сильным в другой области, осваивать которую нужно полгода.

  • Не задавайте вопросы по алгоритмике и математике, если ваш проект в этом правда не нуждается. Это очень популярная тема на собеседованиях, однако как математика, так и алгоритмика нужна в очень узком спектре задач. Я встречал очень сильных в программировании людей, которые не разбирались в этом: математика была давно и забылась, а алгоритмика как не была нужна, так и не стала. Даже если периодически какие-то знания нужны в вашем проекте - насколько они критичны? Любые знания, которые можно быстро нагуглить - просто крошка по сравнению с тем, чему люди учатся годами.

  • Лучше нанять толкового junior’a, чем бестолкового senior’a. Очень большой процент разработчиков остается на одном и том же уровне всю жизнь. Их много, и они бесполезны. Намного лучше нанять свежего разработчика с мозгами, который начнет разбираться в предмете уже через месяц. К сожалению рецептов как найти таких шустряков у меня нет, только после месяца-двух работы с людьми можно о чем-то говорить.

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

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

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

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

Read more
21 Dec 2012

Пример настройки Database Connection Pool

Собственно хочу поделиться и описать конфигурацию DB Pool’a который мы используем в JTalks:

<!--Создаем Ленивый DataSource, который по возможности не будет вытягивать настоящее--> 
    <!--соединение к БД из настоящего пула до тех пор пока это правда необходимо. Например, -->
    <!--если Hibernate достает данные из кеша и не делает запрос в БД, нам не нужно--> 
    <!--настоящее соединение. Однако если не использовать ленивый DataSource, то соединение--> 
    <!--будет вытянуто и транзакция будет начата если метод помечен @Transactional-->
  <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
    <property name="targetDataSource">
    <!--Это собственно настоящий DB Pool. Многие говорят что пул, который Hibernate--> 
    <!--использует по умолчанию не является серьезным и использование его в production-->
    <!--коде не допустимо. Однако C3P0 как раз не является пулом по умочанию, у Hibernate -->
    <!--есть свой внутренний пул для "поиграться", однако это не C3P0 как многие думают! С другими-->
    <!--же пулами не сравнивал на самом деле, поэтому не могу сказать какой лучше,--> 
    <!--C3P0 используется исторически во многих приложениях с Hibernate. -->
      <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="${jdbc.driverClassName}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${JCOMMUNE_DB_USER:root}"/>
        <property name="password" value="${JCOMMUNE_DB_PASS:root}"/>
        <!--Эта опция показывает сколько PreparedStatement'ов должно быть закешировано-->
        <!--на стороне Java, в самом Connection Pool. Имейте в виду что СУБД может -->
        <!--требовать дополнительной настройки для того чтоб эта опция показала себя эффективной-->
        <property name="maxStatements" value="1500"/>
        <!--А это сколько PreparedStatement'ов каждое соединение может кешировать для себя -->
        <property name="maxStatementsPerConnection" value="50"/>
        <!--Вот это самая сложная опция и для того чтоб ее правильно настроить,-->
        <!--нам нужны нагрузочные тесты а также рабочее в PROD приложение-->
        <!--Есть разные стратегии по работе с соединениями которые влияют на оптимальный-->
        <!--размер DB Pool'a, подробней читайте прекрасную книгу Release It!-->
        <property name="maxPoolSize" value="50"/>
        <!--MySQL прибивает соединение если оно не использовалось какое-то время.--> 
        <!--По умолчанию это 8 часов. Дабы избежать неприятных исключений нам -->
        <!--нужно бомбардировать MySQL запросами каждый N часов (минут)-->
        <property name="idleConnectionTestPeriod" value="3600"/>
        <!--Если мы испытываем большую нагрузку и/или запросы выполняются очень долго,-->
        <!--мы сможем обслуживать 50 пользователей одновременно (размер пула), однако -->
        <!--при увеличении нагрузки, клиенты начнут выстраиваться в очередь и просто ждать-->
        <!--заблокированные в synchronized методах внутри DB Pool'a. Дабы мы не оказались-->
        <!--в ситуации, когда мы заблокированы надолго и приложение совсем не отвечает, -->
        <!--через 10 секунд простаивания в очереди выбрасывается исключение и сервер разгружается-->
        <!--от лишних запросов. Да, это исключение и это неприятно, однако лучше отвалится -->
        <!--несколько клиентов (особенно если это DDoS'еры поганые) нежели сайт будет в полной отключке.-->
        <property name="checkoutTimeout" value="10000"/>
        <!--Также соединение может издохнуть. Пул может проверять его на работоспособность-->
        <!--в то время как приложение запрашивает соединение и наоборот - когда возвращает обратно.-->
        <!--Первое - более надежное, второе - более быстрое.-->
        <property name="testConnectionOnCheckin" value="true"/>
      </bean>
    </property>
  </bean>


  • Подробрей о кешировании PreparedStatement

  • Больше о разрыве и тестировании соединений можно прочесть тут, мини проект с примером настроенного пула тут.

  • Если вы получили подобное исключение, значит вам поможет iddleConnectionTestPeriod описанный выше:

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 64,129,968 milliseconds ago.  The last packet sent successfully to the server was 64,129,968 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
    at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1116)
    at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3851)
Read more