Mulgard
Mulgard

Reputation: 10609

Mocking a queue in Kotlin with Mockito does not seem to work

I have a simple named service which uses a queue:

@Named
class OrderFormService @Inject constructor(
        private val repository: OrderFormRepository
) {
    private val queue: Queue<OrderForm> = LinkedList()

    private val logger: Logger = LoggerFactory.getLogger("service")

    fun getNextOrderForm(input: GetNextOrderFormInput): GetNextOrderFormPayload? {
        if (queue.isEmpty()) {
            logger.info("queue is empty")

            val forms: List<OrderForm> = repository.findTop1000ByImageTypeAndImageState(input.type, input.state)

            forms.forEach {
                queue.offer(it)
            }
        }

        if (!queue.isEmpty()) {
            return GetNextOrderFormPayload(queue.poll())
        }

        return null
    }
}

While trying to unit test this I want to mock the queue:

@ExtendWith(MockitoExtension::class)
internal class OrderFormServiceTest {

    @Mock
    private val queue: Queue<OrderForm> = LinkedList()

    @Mock
    lateinit var repository: OrderFormRepository

    @InjectMocks
    lateinit var service: OrderFormService


    @Test
    fun givenValidInputAndFilledQueueWhenGetNextOrderFormThenReturnPayload() {
        // given
        val expected = createOrderForm()
        val expectedPayload = GetNextOrderFormPayload(expected)

        given(queue.isEmpty()).willReturn(false)
        given(queue.poll()).willReturn(expected)

        // when
        val input = GetNextOrderFormInput(ImageType.NUMBER, ImageState.UNCLASSIFIED)
        val result = service.getNextOrderForm(input)

        // then
        assertThat(result).isEqualTo(expectedPayload)
    }
}

But the queue is always empty. So I guess the queue is not getting mocked correctly. What am I doing wrong?

EDIT

Things I have tried:

  1. Making queue not final:
...
var queue: Queue<OrderForm> = LinkedList()
...
  1. Using Mockito.mock:
...
var queue = Mockito.mock(Queue::class.java)

`when`(queue.isEmpty()).thenReturn(false)
`when`(queue.poll()).thenReturn(expected)
...

Upvotes: 1

Views: 463

Answers (1)

Dirk Bolte
Dirk Bolte

Reputation: 662

Your queue is not marked as @Autowired or part of the constructor, thus Mockito cannot mock it.

In order to make this work (haven't verified it though), define your constructor like this:

@Named
class OrderFormService @Inject constructor(
    private val repository: OrderFormRepository,
    private val queue: Queue<OrderForm>
) { }

Now, in order to have the queue initialized in your regular program, you have to define a bean for it, something like:

@Configuration
class QueueConfiguration {
    @Bean
    fun queue() : Queue<OrderForm> = LinkedList()
}

Furthermore, you should keep in concern that @InjectMocks only will use one injection method. So you can't mix constructor initialisation with property setters or field injection.

Also have a look at @MockBean. This replaces the bean globally and can be more convenient to use. It has the drawback that it dirties the context, resulting in context reinitialization and potentially slower tests if they aren't sliced properly.

EDIT:

Another alternative would be to set the mock manually (samples not verified, hope they work work you). I recommend using https://github.com/nhaarman/mockito-kotlin to make the Mockito syntax more kotlinish.

Setting the mock manually requires to make the queue a publicly settable property:

@Named
class OrderFormService @Inject constructor(
        private val repository: OrderFormRepository
) {
    var queue: Queue<OrderForm> = LinkedList()
}

Then, you assign the mock in your test:

internal class OrderFormServiceTest {

    private val queue: Queue<OrderForm> = mock {}

    @Mock
    lateinit var repository: OrderFormRepository

    @InjectMocks
    lateinit var service: OrderFormService

    @BeforeEach
    fun setup() {
        service.queue = queue
    }
}

There's one issue with this though: depending on the framework you use, your OrderFormService might be initialised only once. When setting the queue, you change the global object which might affect other tests. To mitigate this, @DirtiesContext on your test will ensure that the whole context is rebuilt (which impacts test performance). This is more or less the same which @MockBean would do for you (with the same performance impact). You can also clean the object yourself though.

Upvotes: 1

Related Questions