Schlagwort: testing

#6 One thing to look out for when using LiveData and Transformations

If you were to unit test LiveData and LiveData transformations you might write a test like this:

  1. class MainViewModel(initialArticle: Article) : ViewModel() {
  2. private val _articleLiveData = MutableLiveData<Article>()
  3. val articleLiveData: LiveData<Article> = _articleLiveData
  4. val articleTitleLiveData: LiveData<String> = Transformations.map(_articleLiveData) { it.title }
  5. init {
  6. _articleLiveData.value = initialArticle
  7. }
  8. }
  9. class MainViewModelTest {
  10. @get:Rule
  11. var rule: TestRule = InstantTaskExecutorRule()
  12. private lateinit var viewModel: MainViewModel
  13. private val initialArticle =
  14. Article("first Article", "text of first article. blablalbalb. blalbla. bla.")
  15. @Before
  16. fun setup() {
  17. viewModel = MainViewModel(initialArticle)
  18. }
  19. @Test
  20. fun testArticleEmission() {
  21. Assert.assertEquals(initialArticle, viewModel.articleLiveData.value)
  22. }
  23. @Test
  24. fun testArticleTitleEmission() {
  25. Assert.assertEquals(initialArticle.title, viewModel.articleTitleLiveData.value)
  26. }
  27. }
class MainViewModel(initialArticle: Article) : ViewModel() {

    private val _articleLiveData = MutableLiveData<Article>()
    val articleLiveData: LiveData<Article> = _articleLiveData
    val articleTitleLiveData: LiveData<String> = Transformations.map(_articleLiveData) { it.title }

    init {
        _articleLiveData.value = initialArticle
    }
}

class MainViewModelTest {

    @get:Rule
    var rule: TestRule = InstantTaskExecutorRule()

    private lateinit var viewModel: MainViewModel

    private val initialArticle =
        Article("first Article", "text of first article. blablalbalb. blalbla. bla.")

    @Before
    fun setup() {
        viewModel = MainViewModel(initialArticle)
    }

    @Test
    fun testArticleEmission() {
        Assert.assertEquals(initialArticle, viewModel.articleLiveData.value)
    }

    @Test
    fun testArticleTitleEmission() {
        Assert.assertEquals(initialArticle.title, viewModel.articleTitleLiveData.value)
    }
}

But it might surprise you that the 2nd test testArticleTitleEmission will fail. Why is that?

Turns out that transformed LiveData does not emit values without an observer. So to make the test pass you need to adjust it in the following way:

  1. @Test
  2. fun testArticleTitleEmission() {
  3. viewModel.articleTitleLiveData.observeForever { } // This is the important bit!!
  4. Assert.assertEquals(initialArticle.title, viewModel.articleTitleLiveData.value)
  5. }
@Test
fun testArticleTitleEmission() {
    viewModel.articleTitleLiveData.observeForever { } // This is the important bit!!
    Assert.assertEquals(initialArticle.title, viewModel.articleTitleLiveData.value)
}

Note: It doesn’t matter what your observer does, just that it exists.

Now both tests will pass.

#4 Testing: The disadvantage of static methods

What are static methods?

  • Static methods belong to the class rather than an instance of the class.
  • Static methods are not inherited and thus cannot be overridden.
  • You cannot use static methods for declaring a contract via an interface.

These facts are in themselves not bad.

Why are static methods bad for testing? 

Let’s say we want to test the following createCommand method using JUnit Jupiter:

  1. public class Mapper {
  2. public Command createCommand(final Event event) {
  3. return Command.builder()
  4. .commandUuid(UUID.randomUUID().toString())
  5. .id(event.getEventId())
  6. .name(event.getName()).build();
  7. }
  8. }
  9. @Builder
  10. @Getter
  11. @EqualsAndHashCode
  12. public class Event {
  13. private String type;
  14. private String eventId;
  15. private String name;
  16. }
  17. @Builder
  18. @Getter
  19. @EqualsAndHashCode
  20. public class Command {
  21. private String commandUuid;
  22. private String name;
  23. private String id;
  24. }
public class Mapper {
    public Command createCommand(final Event event) {
        return Command.builder()
                .commandUuid(UUID.randomUUID().toString())
                .id(event.getEventId())
                .name(event.getName()).build();
    }
}

@Builder
@Getter
@EqualsAndHashCode
public class Event {
    private String type;
    private String eventId;
    private String name;
}

@Builder
@Getter
@EqualsAndHashCode
public class Command {
    private String commandUuid;
    private String name;
    private String id;
}

But mocking the static UUID.randomUUID() method is not possible with most testing frameworks. This is where the problems starts:

We don’t know what to put as commandUuid because it get’s generated inside the mapping by using a static method. This dependency is hidden for us.

  1. public class TestMapping {
  2. @Test
  3. public void testMapping() {
  4. Mapper mapper = new Mapper();
  5. String eventId = UUID.randomUUID().toString();
  6. String name = "name";
  7. Event event = Event.builder()
  8. .eventId(eventId)
  9. .name(name)
  10. .type("create")
  11. .build();
  12. Command command = mapper.createCommand(event);
  13. Command expected = Command.builder()
  14. .name(name)
  15. .id(eventId)
  16. .commandUuid("Don't know what to do here? ?")
  17. .build();
  18. Assertions.assertEquals(command, expected);
  19. }
  20. }
public class TestMapping {

    @Test
    public void testMapping() {
        Mapper mapper = new Mapper();
        String eventId = UUID.randomUUID().toString();
        String name = "name";
        Event event = Event.builder()
                .eventId(eventId)
                .name(name)
                .type("create")
                .build();
        Command command = mapper.createCommand(event);

        Command expected = Command.builder()
                .name(name)
                .id(eventId)
                .commandUuid("Don't know what to do here? ?")
                .build();

        Assertions.assertEquals(command, expected);
    }
}

So what could do we do instead?

  1. add dependencies for Mockito and Mockito JUnit Jupiter support
  2. Create UuidGenerator class with a generateUuid() method (if you want you could even go with an interface, but let’s keep it simple for now)
  3. inject UuidGenerator instance to the Mapper class.
  4. Mock the UuidGenerators generateUuid method to always return the same uuid.

Our code will now look like this:

  1. public class UuidGenerator{
  2. public String generateUuid(){
  3. return UUID.randomUUID().toString()
  4. }
  5. }
  6. public class Mapper {
  7. private final UuidGenerator uuidGenerator;
  8. public Mapper(UuidGenerator uuidGenerator) {
  9. this.uuidGenerator = uuidGenerator;
  10. }
  11. public Command createCommand(final Event event) {
  12. return Command.builder()
  13. .commandUuid(uuidGenerator.generateUuid())
  14. .id(event.getEventId())
  15. .name(event.getName()).build();
  16. }
  17. }
  18. @ExtendWith(MockitoExtension.class)
  19. public class TestMapping {
  20. private Mapper mapper;
  21. @Mock
  22. private UuidGenerator uuidGenerator;
  23. @BeforeEach
  24. public void before() {
  25. mapper = new Mapper(uuidGenerator);
  26. String commandId = UUID.randomUUID().toString();
  27. Mockito.when(uuidGenerator.generateUuid()).thenReturn(commandId);
  28. }
  29. @Test
  30. public void testMapping() {
  31. String eventId = UUID.randomUUID().toString();
  32. String name = "name";
  33. Event event = Event.builder()
  34. .eventId(eventId)
  35. .name(name)
  36. .type("create")
  37. .build();
  38. Command command = mapper.createCommand(event);
  39. Command expected = Command.builder()
  40. .name(name)
  41. .id(eventId)
  42. .commandUuid(uuidGenerator.generateUuid())
  43. .build();
  44. Assertions.assertEquals(command, expected);
  45. }
  46. }
public class UuidGenerator{
    public String generateUuid(){
       return UUID.randomUUID().toString()
    }
}

public class Mapper {

    private final UuidGenerator uuidGenerator;

    public Mapper(UuidGenerator uuidGenerator) {
        this.uuidGenerator = uuidGenerator;
    }

    public Command createCommand(final Event event) {
        return Command.builder()
                .commandUuid(uuidGenerator.generateUuid())
                .id(event.getEventId())
                .name(event.getName()).build();
    }
}


@ExtendWith(MockitoExtension.class)
public class TestMapping {

    private Mapper mapper;

    @Mock
    private UuidGenerator uuidGenerator;

    @BeforeEach
    public void before() {
        mapper = new Mapper(uuidGenerator);
        String commandId = UUID.randomUUID().toString();
        Mockito.when(uuidGenerator.generateUuid()).thenReturn(commandId);
    }

    @Test
    public void testMapping() {
        String eventId = UUID.randomUUID().toString();
        String name = "name";
        Event event = Event.builder()
                .eventId(eventId)
                .name(name)
                .type("create")
                .build();
        Command command = mapper.createCommand(event);

        Command expected = Command.builder()
                .name(name)
                .id(eventId)
                .commandUuid(uuidGenerator.generateUuid())
                .build();

        Assertions.assertEquals(command, expected);
    }
}

One could argue that we could write a custom equalsData() function or something similar. Or check for all fields separately? But what if we’ll change one of the Event or Command classes? We would always need to think about adjusting these methods and imagine bigger data classes with lots of fields: everything would get very cluttered.

Apart from that this is just an example, this principle is applicable to many more cases.