Hướng dẫn sử dụng Robolectric để viết Unit Test cho ứng dụng Android

Việc chạy unit test trên thiết bị thật hoặc emulator trong Android thường rất chậm do phải đóng gói APK và khởi động toàn bộ môi trường Android. Robolectric giải quyết vấn đề này bằng cách cung cấp một môi trường giả lập trên JVM, thay thế các lời gọi hệ thống Android bằng các "shadow" class – những lớp mô phỏng hành vi của framework gốc nhưng có thể chạy hoàn toàn trên máy ảo Java.

Cấu hình dự án

Trong file build.gradle (module-level), thêm cấu hình sau:

android {
    testOptions {
        unitTests {
            includeAndroidResources = true
            all {
                jvmArgs '-noverify'
            }
        }
    }
}

dependencies {
    testImplementation 'org.robolectric:robolectric:4.3'
}

Với Android Studio 3.3 trở lên, không cần thêm dòng android.enableUnitTestBinaryResources=true vào gradle.properties.

Cấu hình lớp kiểm thử

Mỗi lớp test cần được chú thích với @RunWith(RobolectricTestRunner.class) và có thể tùy chỉnh môi trường qua @Config:

@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.M)
public class MainActivityTest {
    // Các phương thức kiểm thử
}

Các tình huống kiểm thử phổ biến

1. Cấu hình môi trường kiểm thử

Robolectric hỗ trợ nhiều tùy chọn qua @Config:

  • SDK version: @Config(sdk = Build.VERSION_CODES.KITKAT)
  • Application class: @Config(application = CustomApp.class)
  • Tài nguyên đặc tả: @Config(qualifiers = "zh-rCN") để kiểm thử chuỗi ngôn ngữ Trung Quốc

Có thể thay thế chú thích bằng file robolectric.properties đặt trong src/test/resources:

sdk=21
qualifiers=zh-rCN

2. Kiểm thử chuyển Activity

Giả sử có nút bấm trong MainActivity khởi chạy LoginActivity:

@Test
public void shouldStartLoginActivityWhenLoginButtonClicked() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);
    activity.findViewById(R.id.btn_login).performClick();

    Intent startedIntent = shadowOf(RuntimeEnvironment.application)
                              .getNextStartedActivity();
    assertThat(startedIntent.getComponent())
            .isEqualTo(new ComponentName(activity, LoginActivity.class));
}

3. Xử lý Toast và Dialog

Kiểm tra nội dung Toast:

@Test
public void shouldShowToastWithCorrectMessage() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);
    activity.findViewById(R.id.btn_login).performClick();

    assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo("Đăng nhập thành công");
}

Kiểm tra AlertDialog:

@Test
public void shouldDisplayAlertDialog() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);
    activity.findViewById(R.id.btn_login).performClick();

    AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
    assertThat(dialog).isNotNull();
    assertThat(shadowOf(dialog).getMessage()).isEqualTo("Xác nhận đăng nhập?");
}

4. Kiểm thử Fragment

Với Fragment sử dụng thư viện hỗ trợ:

@Test
public void shouldDisplayPassedArgumentInFragment() {
    Bundle args = new Bundle();
    args.putString("name", "Nguyễn Văn A");

    TestFragment fragment = TestFragment.newInstance(args);
    SupportFragmentTestUtil.startFragment(fragment);

    TextView nameView = fragment.getView().findViewById(R.id.tv_name);
    assertThat(nameView.getText().toString()).isEqualTo("Nguyễn Văn A");
}

Yêu cầu thêm phụ thuộc nếu dùng AndroidX hoặc Support Library:

testImplementation 'org.robolectric:shadows-supportv4:4.3'

5. Quản lý vòng đời Activity

Sử dụng ActivityController để điều khiển từng giai đoạn vòng đời:

@Test
public void shouldUpdateStateDuringLifecycle() {
    ActivityController<MainActivity> controller = 
        Robolectric.buildActivity(MainActivity.class);
    
    controller.create();
    assertThat(activity.getState()).isEqualTo("created");

    controller.start();
    assertThat(activity.getState()).isEqualTo("started");

    controller.resume();
    assertThat(activity.getState()).isEqualTo("resumed");
}

6. Kiểm thử BroadcastReceiver

Đối với LocalBroadcastManager:

@Test
public void shouldSaveDataWhenReceivingValidBroadcast() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);
    LocalBroadcastManager manager = 
        ShadowLocalBroadcastManager.getInstance(activity);

    Intent intent = new Intent(MainActivity.ACTION_SAVE_DATA);
    intent.putExtra("user_id", "12345");
    manager.sendBroadcast(intent);

    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
    assertThat(prefs.getString("user_id", "")).isEqualTo("12345");
}

7. Kiểm thử Service

Xác minh việc khởi chạy Service:

@Test
public void shouldStartBackgroundServiceOnButtonClick() {
    MainActivity activity = Robolectric.setupActivity(MainActivity.class);
    activity.findViewById(R.id.btn_start_service).performClick();

    Intent expected = new Intent(activity, BackgroundService.class);
    Intent actual = ShadowApplication.getInstance().getNextStartedService();
    assertThat(actual.getComponent()).isEqualTo(expected.getComponent());
}

8. Thao tác với cơ sở dữ liệu và tệp tin

Robolectric tự động tạo môi trường lưu trữ tạm thời cho các thao tác I/O:

@Test
public void shouldReadWriteFileCorrectly() {
    FileManager manager = new FileManager();
    String testContent = "Nội dung kiểm thử";
    String filePath = Environment.getExternalStorageDirectory() + "/test.txt";

    manager.write(filePath, testContent);
    assertThat(manager.read(filePath)).isEqualTo(testContent);
}

9. Xử lý yêu cầu mạng bất đồng bộ

Để kiểm thử logic xử lý phản hồi API, có thể sử dụng interceptor hoặc mock callback:

@Test
public void shouldShowSuccessMessageOnValidApiResponse() {
    // Thiết lập interceptor trả về JSON mẫu
    TestInterceptor.setMode(TestInterceptor.MODE_SUCCESS);

    MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
    new MusicChannelPresenter(activity).requestChannels();

    // Chạy tác vụ bất đồng bộ ngay lập tức
    ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

    TextView resultView = activity.findViewById(R.id.result_text);
    assertThat(resultView.getText().toString()).isEqualTo("Tải dữ liệu thành công");
}

10. Tùy chỉnh Shadow Class

Tạo lớp shadow cho lớp tùy chỉnh:

@Implements(Person.class)
public class ShadowPerson {
    @RealObject Person realPerson;

    @Implementation
    public String getName() {
        return "Người dùng mặc định"; // Ghi đè giá trị gốc
    }

    public String getOriginalName() {
        return realPerson.name; // Truy cập trực tiếp field
    }
}

Áp dụng trong test:

@Config(shadows = ShadowPerson.class)
@Test
public void shouldReturnMockedName() {
    Person person = new Person("Nguyễn Văn B");
    assertThat(person.getName()).isEqualTo("Người dùng mặc định");
}

11. Tối ưu hóa cấu hình chung

Tạo lớp cơ sở cho tất cả bài kiểm thử:

@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.M)
public abstract class BaseTest {
    protected Application getApp() {
        return RuntimeEnvironment.application;
    }

    protected <T extends Activity> T startActivity(Class<T> cls) {
        return Robolectric.setupActivity(cls);
    }
}

Lưu ý quan trọng

  • Không nên mock các lớp hệ thống Android như Context hay SharedPreferences
  • Tránh kiểm thử layout inflation – tập trung vào logic tương tác
  • Luôn dọn dẹp luồng nền sau mỗi bài test để tránh rò rỉ tài nguyên
  • Sử dụng kết hợp Mockito/PowerMockito để xử lý các phương thức tĩnh hoặc final

Thẻ: Robolectric Android Testing Unit Testing Shadow Classes JUnit4

Đăng vào ngày 12 tháng 7 lúc 11:33