单元测试异步操作

Unit testing asynchronous operations

我正在尝试对将工作委托给后台执行程序的代码进行单元测试。在我将删除方法重构为 return id 列表后,我的单元测试中出现了 运行 问题。当方法抛出 sqlexception 失败时应验证行为的测试。

我以前没有在我的代码中使用 Futures,所以如果这个设计有缺陷,请原谅我。

以下是我的代码。

TaskInteractor.java:

public class TaskInteractor extends AbstractInteractor
    implements TaskContract.Interactor {

    private final TaskRepository mRepository;

    @Inject
    public TaskInteractor(WorkerThread workerThread, MainThread mainThread, TaskRepository repository) {
        super(workerThread, mainThread);
        this.mRepository = repository;
    }
    ...
    @Override
    @android.support.annotation.MainThread
    public void deleteTasks(@NonNull final List<Task> tasks, @NonNull final DeleteTaskCallback callback) {
        try {
        final Future<List<String>> future = mWorkerThread.execute(() ->
                mRepository.softDeleteAllInTransaction(tasks));
        mMainThread.post(() -> {
            try {
                callback.onDeleteSuccess(future.get());
            } catch (InterruptedException | ExecutionException e) {
                Timber.e(e);
                throw new RuntimeException(e.getCause());
            }
        });
    } catch (final SQLiteAbortException e) {
        mMainThread.post(() -> { callback.onAbortException(e); });
        throw e;
    } catch (final SQLiteConstraintException e) {
        mMainThread.post(() -> { callback.onConstraintException(e); });
        throw e;
    } catch (final Exception e) {
        mMainThread.post(() -> { callback.onFailure(e); });
        throw new RuntimeException(e);
    }
    }
}

我收到以下消息:

Wanted but not invoked:
mDeleteTaskCallbackMock.onAbortException(
    <any android.database.sqlite.SQLiteAbortException>
);
-> at com.example.ui.task.ExaminationInteractorTest.whenDeleteFailWithSQLiteAbortException_shouldCallOnAbortFailureCallback(ExaminationInteractorTest.java:173)

However, there was exactly 1 interaction with this mock:
mDeleteTaskCallbackMock.onFailure(
    java.lang.RuntimeException: 
android.database.sqlite.SQLiteAbortException
);
-> at com.example.ui.examination.ExaminationInteractor.lambda$deleteExaminations(ExaminationInteractor.java:79)

这是我的一项测试。

@Test
public void whenDeleteFailWithSQLiteConstraintException_shouldCallOnConstraintFailureCallback() throws Exception {
    doThrow(new SQLiteConstraintException()).when(mRepositoryMock).softDeleteAllInTransaction(ArgumentMatchers.<Task>anyList());

    List<Task> tasks = Arrays.asList(TEST_TASKS);

    mInteractor.deleteTasks(tasks, mDeleteTaskCallback);
    verify(mDeleteTaskCallback).onConstraintException(
        any(SQLiteConstraintException.class));
}

执行由具有以下实现的测试替身完成。

FakeWorkerThread.java:

/**
* Just runs the commands without invoking other threads
*/
public class FakeWorkerThread implements WorkerThread {

    @Override
    public void execute(Runnable interactor) {
        interactor.run();
    }

    @Override
    public <T> Future<T> execute(Callable<T> callable) throws Exception {
        RunnableFuture<T> ftask = new FutureTask<T>(callable);
        execute(ftask);
        return ftask;
    }
}

在此代码段中

try {
    final Future<List<String>> future = mWorkerThread.execute(() ->
            mRepository.softDeleteAllInTransaction(tasks));
    mMainThread.post(() -> {
        try {
            callback.onDeleteSuccess(future.get()); // LINE 1
        } catch (InterruptedException | ExecutionException e) {
            Timber.e(e);
            throw new RuntimeException(e.getCause()); // LINE 2
        }
    });

您正在将 Callable 传递给工作人员。它的 call() 抛出的任何异常都将包装在 ExecutionException 中并存储在 Future 中。 然后,当你调用 future.get() 并且任务完成时,它会抛出 ExecutionException.

所以发生的事情如下:

  1. future.get() 抛出一个 ExecutionException 包装一个 SQLiteConstraintException (LINE 1);
  2. SQLiteConstraintException 被包裹在 RuntimeException(第 2 行)中;
  3. 由于 RuntimeException 与前 2 个 catch 子句不匹配,它最终在最后一个 catch 中处理:catch (final Exception e) {};
  4. 如测试输出所示,mDeleteTaskCallbackMock(RuntimeException) 被调用;

建议你改成下面的:

try {
  callback.onDeleteSuccess(future.get());
} catch (InterruptedException | ExecutionException e) {
  Timber.e(e);
  throw e.getCause(); //<--- throw the unwrapped exception
}