2013-03-28

Не ванильное тестирование

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

Продолжение поста о неванильном тестировании. Полный текст примеров: исходный код, тестовый фреймворк, тестовый сценарий. Данный пример повторяет реальную архитектуру, имеется мешанина с логикой, хотя там и написан полный бред. Плюс работа с базой лишь обозначена, а в тесте замокана для удобства. Тест написан только один, просто чтобы продемонстрировать подход - остальные тесты один-в-один.



Workflow

1. CollectorDaemon

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

Демоны CallCollectorDaemon и ConferenceCollectorDaemon (заглушка с логикой идентичной CCD) подписываются каждый на свою группу событий, после чего события начинают приходить в метод onEvent.
     public CallCollectorDaemon(CallRouter router) {
        this.router = router;
        subscribe("CREATE", "BRIDGE", "DESTROY");
    }

    @Override
    public void onEvent(HashMap<String, String> rawEvent) {
    ...

Задача демона, распарсить события в соответствующие DTO.

2. Router

Router хранит справочник всех ведущихся сессий, создает новые сессии, сливает разние сессии в одну, если они оказываются связанными. После того как подходящая сессия найдена, событие отправляется в сессию для извлечения данных.

3. Call

Call - сессия, которая может породить в процессе несколько записей. При каждом поступившем событии данные извлекаются и проверяются два условия: на готовность записи и завершенность сессии.

4-6. Router

Если в сессии после события оказывается готовая запись, то она извлекается роутером и заполняется данными из базы
private void testCallReady(Call call) {
        // Check for ready record
        if (call.getReady().size() > 0) {
            ready.addAll(call.getReady());
            call.getReady().clear();
        }
        ...
    }

 

... 

public Record pullRecord(UserDao userDao) {
        Record record = ready.pollFirst();
        ...

        User user = userDao.findUserByName(record.getNumber());
        if (user == null) {
            user = userDao.findUserByTerminal(record.getNumber());
        }

        if (user != null) {
            record.setUser(user);
        }

        return record;
    }

7. CollectorDaemon

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

Тестовый фреймворк

Тестер

Тестер работает только со вторым слоем, потому, что тестировать первый нет особого смысла. Сначала загружается и парсится yaml файл при помощи snakeyaml:

    protected List<Object> loadEvents(String yaml) {
        InputStream is = getClass().getClassLoader().getResourceAsStream(yaml);
        return (List<Object>) new Yaml().load(is);
    }

Затем объекты засылаются в роутер если это событие, либо сравниваются с готовыми записями, если это запись:
    protected void testEventFlow(List<Object> events) {
        HashMap<Long, User> userHashMap = new HashMap<Long, User>();
        for (User u : userDao.findAll()) {
            userHashMap.put(u.getId(), u);
        }

        CallRouter er = new CallRouter();
        for (Object event : events) {
            if (event instanceof CreateEvent) {
                er.addEvent((CreateEvent) event);
            } else if (event instanceof BridgeEvent) {
                er.addEvent((BridgeEvent) event);
            } else if (event instanceof DestroyEvent) {
                er.addEvent((DestroyEvent) event);
            } else if (event instanceof Record) {
                Record record = er.pullRecord(userDao);
                ((Record) event).setUser(userHashMap.get(((Record) event).getUser().getId()));
                compare((Record) event, record);
            }
        }
        assertFalse(er.hasNewRecord());
    }
Для удобства сравнения, поле юзер подменяется по первичному ключу.

Сценарий

Представляет собой типичный yaml файл:

- !!com.blazer.scenario.event.DestroyEvent
    id: 541852
    chain: 1239641
    date: 1333291427000000
    service: USER
    name: terminal2
    hangup: !!com.blazer.scenario.domain.HangupType NORMAL

- !!com.blazer.scenario.domain.Record
    id: 541852
    chain: 1239641
    begin: 1333291338000000
    end: 1333291427000000
    user: !!com.blazer.scenario.domain.User
        id: 2
    number: terminal2
    legB: 541852
    hangup: !!com.blazer.scenario.domain.HangupType NORMAL

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

Проблемные места

When we have to simulate the software to test it
Клубки логики сосредоточены в трех местах: в роутере на входе (отправка события в нужную сессию) и выходе (заполнение данными) и сессии (пополнение данных и генерация готовых записей). Сам тестовый фреймворк почти не претерпел изменений с эволюцией кода, в отличии от yaml-сценариев. Есть опасение что реальный данные, посылаемые от FreeSWITCH могут со временем разойтись с тестовыми сценариями: тесты будут проходить, но на реальной системе будет мешанина.