Покрытие кода тестами
От: Dziman США http://github.com/Dziman
Дата: 11.12.15 11:49
Оценка:
У меня тут случился затуп и/или недопонимание:
Тестовая задача получить по rest информацию, отправить ее в message queue, получить оттуда и прочитав файл вернуть допинформацию если она есть. Код вышел таким:
https://bitbucket.org/Dziman/tt/src/2a84a61783ffa8907a0862b84c6822c3bd809b8e/?at=client_info
Что тут можно и имеет смысл тестировать юнит тестами?
avalon 1.0rc3 build 430, zlib 1.2.5
Отредактировано 11.12.2015 11:51 Dziman . Предыдущая версия .
Re: Покрытие кода тестами
От: · Великобритания  
Дата: 11.12.15 13:42
Оценка: 4 (1)
Здравствуйте, Dziman, Вы писали:

D> У меня тут случился затуп и/или недопонимание:

D> Тестовая задача получить по rest информацию, отправить ее в message queue, получить оттуда и прочитав файл вернуть допинформацию если она есть. Код вышел таким:
D> https://bitbucket.org/Dziman/tt/src/2a84a61783ffa8907a0862b84c6822c3bd809b8e/?at=client_info
D> Что тут можно и имеет смысл тестировать юнит тестами?
ClientInfoManagementController покрывается хорошо. Можно покрыть что возвращаемый статус NOT_FOUND или OK в зависимости от поведения замоканного cardDetailsService.
CardDetailsServiceCSVImpl покрывается тоже. Например, такой тест: делаем "createClientInfo" и убеждаемся что getClientInfo возвращает то что надо. Скажем, этот тест проверит, что ты не перепутаешь колонки name и surname во время записи и чтения.
Ну и тесты поиска, возвращения списка, и т.п.
ClientInfoService покрывается тоже. Например, убедиться что correlationId не потерялся, что setSuccess проставляется правильно false/true, setCardNumber правильный и т.п.
ClientInfoServiceJMSImpl тоже можно покрыть, что, например, выбор correlationId происходит такой же. Ну и даже можно запилить тест, котоый проверит, что два запроса используют разные correlationId.
А однострочные методы с одним вариантом исполнения... да... можно и не тестировать.
avalon/1.0.432
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: Покрытие кода тестами
От: Dziman США http://github.com/Dziman
Дата: 11.12.15 14:18
Оценка:
Здравствуйте, ·, Вы писали:

·> D> У меня тут случился затуп и/или недопонимание:

·> D> Тестовая задача получить по rest информацию, отправить ее в message queue, получить оттуда и прочитав файл вернуть допинформацию если она есть. Код вышел таким:
·> D> https://bitbucket.org/Dziman/tt/src/2a84a61783ffa8907a0862b84c6822c3bd809b8e/?at=client_info
·> D> Что тут можно и имеет смысл тестировать юнит тестами?

·> ClientInfoManagementController покрывается хорошо. Можно покрыть что возвращаемый статус NOT_FOUND или OK в зависимости от поведения замоканного cardDetailsService.

Мне кажется что если мы мокаем cardDetailsService, то весь этот тест суть то же что и тестирование простых аксессоров.

·> CardDetailsServiceCSVImpl покрывается тоже. Например, такой тест: делаем "createClientInfo" и убеждаемся что getClientInfo возвращает то что надо. Скажем, этот тест проверит, что ты не перепутаешь колонки name и surname во время записи и чтения.

·> Ну и тесты поиска, возвращения списка, и т.п.
·> ClientInfoService покрывается тоже. Например, убедиться что correlationId не потерялся, что setSuccess проставляется правильно false/true, setCardNumber правильный и т.п.
·> ClientInfoServiceJMSImpl тоже можно покрыть, что, например, выбор correlationId происходит такой же. Ну и даже можно запилить тест, котоый проверит, что два запроса используют разные correlationId.
·> А однострочные методы с одним вариантом исполнения... да... можно и не тестировать.

ОК, описанные тесты вполне логичны и проверяют что-то значимое. Но в моем понимании это ни разу не юнит тесты, а интеграционные.
avalon 1.0rc3 build 430, zlib 1.2.5
Re[3]: Покрытие кода тестами
От: · Великобритания  
Дата: 11.12.15 14:36
Оценка:
Здравствуйте, Dziman, Вы писали:

D> Мне кажется что если мы мокаем cardDetailsService, то весь этот тест суть то же что и тестирование простых аксессоров.

Нет. Тестируется, что ты, например, не падаешь по NPE если элемент не найден, а возвращаешь ожидаемый 404.

D> ОК, описанные тесты вполне логичны и проверяют что-то значимое. Но в моем понимании это ни разу не юнит тесты, а интеграционные.

Интеграционными они будут если у тебя будет несколько классов собирается. Ты же мокаешь все зависимости. Т.е. не надо запускать JMS-брокер и т.п., а просто заинжектить моки.
Хотя, в принципе да, тест для CardDetailsServiceCSVImpl можно назвать интеграционным, ибо там файл в файловой системе будет использоваться...
avalon/1.0.432
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[4]: Покрытие кода тестами
От: Dziman США http://github.com/Dziman
Дата: 11.12.15 14:48
Оценка:
Здравствуйте, ·, Вы писали:

·> D> Мне кажется что если мы мокаем cardDetailsService, то весь этот тест суть то же что и тестирование простых аксессоров.


·> Нет. Тестируется, что ты, например, не падаешь по NPE если элемент не найден, а возвращаешь ожидаемый 404.


·> D> ОК, описанные тесты вполне логичны и проверяют что-то значимое. Но в моем понимании это ни разу не юнит тесты, а интеграционные.


·> Интеграционными они будут если у тебя будет несколько классов собирается. Ты же мокаешь все зависимости. Т.е. не надо запускать JMS-брокер и т.п., а просто заинжектить моки.

·> Хотя, в принципе да, тест для CardDetailsServiceCSVImpl можно назвать интеграционным, ибо там файл в файловой системе будет использоваться...

Так дело в том что если замокать все внешнее тип JMS–брокера, storage (работа с csv), то тесты получаются
  Response r = new Response();
  when(mock.method()).thenReturn(r);
  assertEquals(r, testObj.call()); // call() { return mock.method();}

и смысла в нем чут менее чем нет.
avalon 1.0rc3 build 430, zlib 1.2.5
Re[5]: Покрытие кода тестами
От: · Великобритания  
Дата: 11.12.15 15:02
Оценка:
Здравствуйте, Dziman, Вы писали:

D> Так дело в том что если замокать все внешнее тип JMS–брокера, storage (работа с csv), то тесты получаются

D>
D>   Response r = new Response();
D>   when(mock.method()).thenReturn(r);
D>   assertEquals(r, testObj.call()); // call() { return mock.method();}
D>

D> и смысла в нем чут менее чем нет.
Ну почему же. Вот рассмотрим ClientInfoService. Там пара десятков значимых строк кода, налажать есть где, совсем не "call() { return mock.method();}"
Надо замокать jmsTemplate и проверить, что вызывается setJMSCorrelationID с правильным id, нужно проверить что ненайденный cardNumber не вызывает NPE, а правильно проставляет setSuccess.
Ещё конструируется сложный InfoResponse объект: надо проверить, что никакие сеттеры не забыты.
avalon/1.0.432
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[6]: Покрытие кода тестами
От: Dziman США http://github.com/Dziman
Дата: 11.12.15 15:57
Оценка:
Здравствуйте, ·, Вы писали:

·> Ну почему же. Вот рассмотрим ClientInfoService. Там пара десятков значимых строк кода, налажать есть где, совсем не "call() { return mock.method();}"

·> Надо замокать jmsTemplate и проверить, что вызывается setJMSCorrelationID с правильным id, нужно проверить что ненайденный cardNumber не вызывает NPE, а правильно проставляет setSuccess.
·> Ещё конструируется сложный InfoResponse объект: надо проверить, что никакие сеттеры не забыты.

Попробуем рассмотреть. Итак, имеем класс

package com.meawallet.secured.service;

@Service
public class ClientInfoService implements MessageListener {
    @Autowired
    private JmsTemplate jmsTemplate;
    @Autowired
    private Queue clientInfoResponseQueue;

    @Autowired
    private CardDetailsService cardDetailsService;

    @Override
    public void onMessage(Message message) {
        if (message instanceof ObjectMessage) {
            try {
                InfoRequest infoRequest = (InfoRequest) ((ObjectMessage) message).getObject();
                final String correlationId = message.getJMSCorrelationID();
                final InfoResponse infoResponse = process(infoRequest);
                jmsTemplate.send(clientInfoResponseQueue, session -> {
                    ObjectMessage objectMessage = session.createObjectMessage(infoResponse);
                    objectMessage.setJMSCorrelationID(correlationId);
                    return objectMessage;
                });
            } catch (JMSException ex) {
                throw new RuntimeException(ex);
            }
        } else {
            throw new IllegalArgumentException("Message must be of type ObjectMessage");
        }
    }

    InfoResponse process(InfoRequest infoRequest) {
        InfoResponse result = new InfoResponse();
        Info resultInfo = new Info();
        List<CardDetails> resultCardDetails = infoRequest.getInfo().getCardDetails().stream()
                .map(cardDetails -> {
                    Optional<CardDetails> optionalClientInfo = cardDetailsService.getClientInfo(cardDetails.getCardNumber());
                    return optionalClientInfo.orElseGet(() -> {
                        CardDetails clientInfo = new CardDetails();
                        clientInfo.setSuccess(false);
                        clientInfo.setCardNumber(cardDetails.getCardNumber());
                        return clientInfo;
                    });
                })
                .collect(Collectors.toList());

        resultInfo.setCardDetails(resultCardDetails);
        result.setInfo(resultInfo);
        return result;
    }
}

onMessage в целом юнит тестом мы проверить не можем никак, т.к. там идет работа с JMS. Т.е. этот метод остается для интеграционных тестов.
process уже имеет некоторую логику и его вполне можно поюниттестировать

package com.meawallet.secured.service;

public class ClientInfoServiceTest {

    @InjectMocks
    private ClientInfoService clientInfoService;

    @Mock
    private CardDetailsService cardDetailsService;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testEmptyRequestProcess() {
        InfoRequest request = prepareEmptyInfoRequest();
        InfoResponse response = clientInfoService.process(request);
        assertNotNull(response);
        assertEquals(0, response.getInfo().getCardDetails().size());
    }

    @SuppressWarnings("unused")
    @Test
    public void testAllInfoFound() {
        InfoRequest request = prepareEmptyInfoRequest();

        CardDetails cardDetails1 = prepareCardDetails(request, "1", "Name 1", "Surname 1");
        CardDetails cardDetails2 = prepareCardDetails(request, "2", "Name 2", "Surname 2");
        CardDetails cardDetails3 = prepareCardDetails(request, "3", "Name 3", "Surname 3");

        InfoResponse response = clientInfoService.process(request);
        List<CardDetails> responseCardDetails = response.getInfo().getCardDetails();

        assertNotNull(response);
        assertEquals(3, responseCardDetails.size());

        List<CardDetails> responseCardDetails2 = responseCardDetails.stream()
                .filter(cardDetails -> cardDetails2.getCardNumber().equals(cardDetails.getCardNumber()))
                .collect(Collectors.toList());

        assertEquals(1, responseCardDetails2.size());
        assertEquals(true, responseCardDetails2.get(0).getSuccess());
        assertEquals("Name 2", responseCardDetails2.get(0).getInfo().getName());
    }
    
    @SuppressWarnings("unused")
    @Test
    public void testAllInfoNotFound() {
        InfoRequest request = prepareEmptyInfoRequest();

        CardDetails cardDetails1 = prepareNotFoundCardDetails(request, "1");
        CardDetails cardDetails2 = prepareNotFoundCardDetails(request, "2");
        CardDetails cardDetails3 = prepareNotFoundCardDetails(request, "3");

        InfoResponse response = clientInfoService.process(request);
        List<CardDetails> responseCardDetails = response.getInfo().getCardDetails();

        assertNotNull(response);
        assertEquals(3, responseCardDetails.size());

        List<CardDetails> responseCardDetails2 = responseCardDetails.stream()
                .filter(cardDetails -> cardDetails2.getCardNumber().equals(cardDetails.getCardNumber()))
                .collect(Collectors.toList());

        assertEquals(1, responseCardDetails2.size());
        assertEquals(false, responseCardDetails2.get(0).getSuccess());

    }

    @Test
    public void testMixedFound() {
        InfoRequest request = prepareEmptyInfoRequest();

        CardDetails cardDetails1 = prepareCardDetails(request, "1", "Name 1", "Surname 1");
        CardDetails cardDetails2 = prepareNotFoundCardDetails(request, "2");
        CardDetails cardDetails3 = prepareNotFoundCardDetails(request, "3");
        CardDetails cardDetails4 = prepareCardDetails(request, "4", "Name 4", "Surname 4");

        InfoResponse response = clientInfoService.process(request);

        List<CardDetails> responseCardDetails = response.getInfo().getCardDetails();

        assertNotNull(response);
        assertEquals(4, responseCardDetails.size());

        List<CardDetails> responseCardDetails2 = responseCardDetails.stream()
                .filter(cardDetails -> cardDetails2.getCardNumber().equals(cardDetails.getCardNumber()))
                .collect(Collectors.toList());

        assertEquals(1, responseCardDetails2.size());
        assertEquals(false, responseCardDetails2.get(0).getSuccess());

        List<CardDetails> responseCardDetails4 = responseCardDetails.stream()
                .filter(cardDetails -> cardDetails4.getCardNumber().equals(cardDetails.getCardNumber()))
                .collect(Collectors.toList());

        assertEquals(1, responseCardDetails4.size());
        assertEquals(true, responseCardDetails4.get(0).getSuccess());
        assertEquals("Name 4", responseCardDetails4.get(0).getInfo().getName());
    }

    private CardDetails prepareNotFoundCardDetails(InfoRequest request, String cardNumber) {
        CardDetails result = new CardDetails();
        result.setCardNumber(cardNumber);
        result.setSuccess(false);

        request.getInfo().getCardDetails().add(result);
        when(cardDetailsService.getClientInfo(cardNumber)).thenReturn(Optional.empty());

        return result;
    }

    private CardDetails prepareCardDetails(InfoRequest request, String cardNumber, String name, String surname) {
        CardDetails result = new CardDetails();
        result.setCardNumber(cardNumber);
        PersonInfo personInfo = new PersonInfo(name, surname);
        result.setInfo(personInfo);
        result.setSuccess(true);

        request.getInfo().getCardDetails().add(result);
        when(cardDetailsService.getClientInfo(cardNumber)).thenReturn(Optional.of(result));

        return result;
    }

    private InfoRequest prepareEmptyInfoRequest() {
        InfoRequest request  = new InfoRequest();
        Info info = new Info();
        List<CardDetails> requestCardDetails = new ArrayList<>();
        info.setCardDetails(requestCardDetails);
        request.setInfo(info);
        return request;
    }
}


Всё. У меня больше нет никаких идей и фантазии что можно тестировать юнит тестами в этом приложении
avalon 1.0rc3 build 430, zlib 1.2.5
Re[7]: Покрытие кода тестами
От: · Великобритания  
Дата: 11.12.15 16:34
Оценка:
Здравствуйте, Dziman, Вы писали:

D> ·> Ну почему же. Вот рассмотрим ClientInfoService. Там пара десятков значимых строк кода, налажать есть где, совсем не "call() { return mock.method();}"

D> ·> Надо замокать jmsTemplate и проверить, что вызывается setJMSCorrelationID с правильным id, нужно проверить что ненайденный cardNumber не вызывает NPE, а правильно проставляет setSuccess.
D> ·> Ещё конструируется сложный InfoResponse объект: надо проверить, что никакие сеттеры не забыты.

D> Попробуем рассмотреть. Итак, имеем класс


D>
D> package com.meawallet.secured.service;

D> @Service
D> public class ClientInfoService implements MessageListener {
D>     @Autowired
D>     private JmsTemplate jmsTemplate;
D>     @Autowired
D>     private Queue clientInfoResponseQueue;

D>     @Autowired
D>     private CardDetailsService cardDetailsService;

D>     @Override
D>     public void onMessage(Message message) {
D>         if (message instanceof ObjectMessage) {
D>             try {
D>                 InfoRequest infoRequest = (InfoRequest) ((ObjectMessage) message).getObject();
D>                 final String correlationId = message.getJMSCorrelationID();
D>                 final InfoResponse infoResponse = process(infoRequest);
D>                 jmsTemplate.send(clientInfoResponseQueue, session -> {
D>                     ObjectMessage objectMessage = session.createObjectMessage(infoResponse);
D>                     objectMessage.setJMSCorrelationID(correlationId);
D>                     return objectMessage;
D>                 });
D>             } catch (JMSException ex) {
D>                 throw new RuntimeException(ex);
D>             }
D>         } else {
D>             throw new IllegalArgumentException("Message must be of type ObjectMessage");
D>         }
D>     }

D>     InfoResponse process(InfoRequest infoRequest) {
D>         InfoResponse result = new InfoResponse();
D>         Info resultInfo = new Info();
D>         List<CardDetails> resultCardDetails = infoRequest.getInfo().getCardDetails().stream()
D>                 .map(cardDetails -> {
D>                     Optional<CardDetails> optionalClientInfo = cardDetailsService.getClientInfo(cardDetails.getCardNumber());
D>                     return optionalClientInfo.orElseGet(() -> {
D>                         CardDetails clientInfo = new CardDetails();
D>                         clientInfo.setSuccess(false);
D>                         clientInfo.setCardNumber(cardDetails.getCardNumber());
D>                         return clientInfo;
D>                     });
D>                 })
D>                 .collect(Collectors.toList());

D>         resultInfo.setCardDetails(resultCardDetails);
D>         result.setInfo(resultInfo);
D>         return result;
D>     }
D> }
D>

D> onMessage в целом юнит тестом мы проверить не можем никак, т.к. там идет работа с JMS. Т.е. этот метод остается для интеграционных тестов.
D> process уже имеет некоторую логику и его вполне можно поюниттестировать

D> Всё. У меня больше нет никаких идей и фантазии что можно тестировать юнит тестами в этом приложении

Да какие-то странные тесты вообще. Создаёшь кучу prepareCardDetails, но не используешь. SuppressWarnings накой?!
Юнит-тесты — это whitebox-тесты. Т.е. ты должен смотреть на реализацию и писать.
Кстати, JmsTemplate это реализация, а зависеть надо от интерфейса, JmsOperations.

Я бы начал с такого теста:

 public class ClientInfoServiceTest {
 
     @InjectMocks
     private ClientInfoService testSubject;
 
     @Mock
     private CardDetailsService cardDetailsService;
 
     @Mock
     private JmsOperations jmsOperations;
     @Mock
     private Queue clientInfoResponseQueue;
     @Captor
     private ArgumentCaptor<MessageCreator> creatorCaptor;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
        doNothing().when(jmsOperations.send(clientInfoResponseQueue, creatorCaptor.capture());
     }

     @Test public shouldReturnExistingDetails()
     {
        //given
        Message msg = new ObjectMessage();
        msg.setObject(new InfoRequest()....withClinetInfo);
        msg.setJMSCorrelationID("12345");
        when(cardDetailsService.getClientInfo("cardNo1")).willReturn(...);
        //when
        testSubject.onMessage(msg);
        MessageCreator creator = creatorCaptor.getValue();
        ObjectMessage actual = creator.createMessage(session);
        //then
        assertEquals("12345", message.getCorrelationID());
        assertEquals(..., message.getObject().getInfo...);
     }


Потом бы выносил общий код из тестов в приватные методы, простенькие mock-классы, например Session явно будет переиспользоваться из многих мест, поди и готовый класс уже есть в Спринге?..
avalon/1.0.432
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.