How to write Integration test to check contents in MDC context?
Asked Answered
E

3

6

Here is the code which I use to add filter to headers and add a UUID

@Slf4j
public class ReqTxIdFilterImpl implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        List<String> headerNames = Collections.list(request.getHeaderNames());
        String requestTxId = "";
        if(!headerNames.isEmpty()){
            requestTxId = request.getHeader(
                    headerNames.stream()//
                            .filter(header -> header.contains("txId"))
                            .findAny()
                            .orElse("")//
                    );
        }
        if (StringUtils.isEmpty(requestTxId)) {
            requestTxId = UUID.randomUUID().toString();
        }
        MDC.put("txId", requestTxId);
        filterChain.doFilter(servletRequest, servletResponse);
        MDC.clear();
    }
}

I use spring boot and MockMvc to test APIs

@Autowired
    private MockMvc mockMvc;

    @Test
    public void test_generatePolicyNumber() throws Exception {
        MvcResult mvcResult = this.mockMvc.perform(post("/test"))
                .header("txId", "test-id")
                .andDo(print()).andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andReturn();
        Assert.assertTrue(mvcResult.getResponse().getContentAsString().contains("test"));
    }

I also want to check the MDC context and check if this test-id is set as txId in the MDC context map and verify it. Is it possible ?

Eolith answered 6/1, 2020 at 12:32 Comment(0)
X
3

Monitor events and MDCs

Use Appender to log your events and MDCs.

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

/**
 * ref: https://www.baeldung.com/junit-asserting-logs
 *      https://www.baeldung.com/custom-logback-appender
 */
public class LogTracker extends AppenderBase<ILoggingEvent> {
    private final List<ILoggingEvent> events = Collections.synchronizedList(new ArrayList<>());

    @Override
    protected void append(ILoggingEvent event) {
        events.add(event);
    }

    public List<ILoggingEvent> getEvents() {
        return events;
    }

    public static LogTracker getInstance(Class<?> loggerName, Level level) {
        var logger = (Logger) LoggerFactory.getLogger(loggerName);
        var tracker = new LogTracker();
        tracker.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
        logger.setLevel(level);
        logger.addAppender(tracker);
        tracker.start();
        return tracker;
    }

    public static LogTracker getInstance(Class<?> loggerName) {
        return getInstance(loggerName,Level.DEBUG);
    }
}

And then iterate your events and assert.

   // init Appender
   var tracker = LogTracker.getInstance(YourClass.class, Level.DEBUG);
   
    // run something
    ...


   // check logs
   tracker.getEvents().forEach((event) -> {
      var mdcMap=event.getMDCPropertyMap()
      // assert...
   });
Xenogamy answered 14/12, 2021 at 15:30 Comment(0)
S
1

It's a bit complicated what you want to do, but a possible solution will be to use an argument captor.

E.g:

public class ReqTxIdFilterImpl implements Filter {

    @Override
    public void doFilter(...) {
       // rest of code
       addIdToMdc(requestTxId);
       filterChain.doFilter(servletRequest, servletResponse);
       MDC.clear();
    }

    protected void addIdToMdc(String requestTxId) { 
       MDC.put("txId", requestTxId);  
    }
}

In the test class:

    @Test
    public void test_generatePolicyNumber() throws Exception {
      ...
      ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
      Mockito.verify(filter).addIdToMdc(captor.capture());

      //get the String object added to MDC using ArgumentCaptor
      String actual = captor.getValue();
      Assert.assertThat(actual).isEqualTo(expectedId);
   }

Please note that you need to inject the filter in your TestClass, which I'm not sure if it's doable in your case.

An alternative solution would be to extract the logic for composing that Id into another component and test that class or call an ResultCaptor for it.: https://mcmap.net/q/499063/-mockito-is-there-a-way-of-capturing-the-return-value-of-stubbed-method

Shira answered 6/1, 2020 at 14:2 Comment(1)
getting error java.lang.StackOverflowError at org.apache.logging.slf4j.MDCContextMap.put(MDCContextMap.java:39) at org.apache.logging.log4j.ThreadContext.put(ThreadContext.java:245)Collegium
D
1

I achieved this by returning the MDC context map from the test controller, and verifying the resulting json structure. Check out below complete test code (sorry its in Kotlin, but easy to understand I hope) :

internal class CorrelationIdFilterTest {

    private lateinit var mockMvc: MockMvc

    /* Test Controller to return MDC context */
    @RestController
    private class TestController {
        @GetMapping("/test")
        fun test(): Map<String, String>? = MDC.getCopyOfContextMap()
    }

    @BeforeEach
    fun setUp() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(TestController())
            .addFilter<StandaloneMockMvcBuilder>(CorrelationIdFilter())
            .build()
    }

    @Test
    fun `given no correlation id is sent one will be created`() {
        mockMvc.perform(get("/test"))
            .andExpect(status().isOk)
            .andExpect(header().exists(HEADER_CORRELATION_ID))
    }

    @Test
    fun `given a correlation id is sent it will be present in response and MDC`() {
        mockMvc.perform(
            get("/test")
                .header(HEADER_CORRELATION_ID, "a correlation id")
        )
            .andExpect(status().isOk)
            .andExpect(header().string(HEADER_CORRELATION_ID, "a correlation id"))
            .andExpect(jsonPath("$.$MDC_CORRELATION_ID").value("a correlation id"))

        assertNull(MDC.get(MDC_CORRELATION_ID)) // check if MDC is cleaned up
    }
}

Dusky answered 28/3, 2021 at 21:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.