Unit Testing (JUnit & Mockito)
A unit test checks one small piece of logic — a single method or class — in isolation, running on the plain JVM without a device or emulator. For RecipeBox, that means testing things like "does the Repository correctly filter recipes by category" without ever touching Room, the network, or the Android framework.
Adding the Dependencies
// app/build.gradle
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.12.0'
}
The src/test/java Source Set
Unit test classes live under src/test/java, a source set that Gradle runs directly on the local JVM (via the test Gradle task), which is why these tests are fast — no emulator boot, no APK install.
A First JUnit Test
JUnit4 marks test methods with @Test, runs a method annotated @Before ahead of every test, and checks outcomes with assertEquals and friends.
public class RecipeValidatorTest {
private RecipeValidator validator;
@Before
public void setUp() {
validator = new RecipeValidator();
}
@Test
public void emptyTitle_isInvalid() {
Recipe recipe = new Recipe();
recipe.title = "";
boolean result = validator.isValid(recipe);
assertEquals(false, result);
}
@Test
public void validRecipe_isValid() {
Recipe recipe = new Recipe();
recipe.title = "Pancakes";
recipe.prepMinutes = 15;
assertTrue(validator.isValid(recipe));
}
}
- @Before setUp() runs before each @Test method, giving every test a fresh RecipeValidator instance.
- assertEquals(expected, actual) fails the test (with a clear diff) when the two values don't match.
- Right-clicking the class (or a single method) in Android Studio and choosing Run executes it directly, showing green/red results in seconds.
Mocking a Repository with Mockito
A RecipeBox ViewModel depends on a RecipeRepository, which in turn depends on Room and the network — dependencies a unit test shouldn't need to actually run. Mockito creates a fake stand-in object that returns whatever the test tells it to.
public class RecipeViewModelTest {
@Test
public void loadRecipes_exposesRepositoryData() {
RecipeRepository mockRepo = mock(RecipeRepository.class);
List<Recipe> fakeRecipes = Arrays.asList(new Recipe("Pancakes"), new Recipe("Omelette"));
when(mockRepo.getAllRecipes()).thenReturn(fakeRecipes);
RecipeViewModel viewModel = new RecipeViewModel(mockRepo);
List<Recipe> result = viewModel.loadRecipes();
assertEquals(2, result.size());
verify(mockRepo).getAllRecipes();
}
}
- mock(RecipeRepository.class) creates a fake object satisfying the RecipeRepository type, with no real implementation.
- when(...).thenReturn(...) stubs a method call: whenever getAllRecipes() is called on the mock, it returns fakeRecipes instead of hitting Room.
- verify(mockRepo).getAllRecipes() confirms the ViewModel actually called that method — not just that the final result happened to look right.
What NOT to Unit Test
Plain JUnit unit tests run on the local JVM, which has no real Android framework classes — Context, View, Activity, and Room's actual database engine either don't exist or throw "not mocked" errors when touched directly from src/test/java.
Code that genuinely needs those classes belongs in Robolectric tests (Android framework simulated on the JVM) or Espresso instrumented tests (a real device or emulator) — both covered in the next two lessons.
Unit Test vs Instrumented Test
| Unit test (this lesson) | Instrumented test | |
|---|---|---|
| Source set | src/test/java | src/androidTest/java |
| Runs on | Local JVM | Real device / emulator |
| Speed | Fast (no APK install) | Slower (installs and launches the app) |
| Android framework classes | Not available | Fully available |
| Good for | Validators, Repositories, ViewModels (with mocks) | UI interactions, real Room queries, full app flows |