Cookie と実際のサービス呼び出しは、統合テストでテストされます。これらのビットが単体テストには不適切であると考えるのは正しいです。
私がやりたいのは、優れたデリゲート (統合ポイントがまだインターフェイスを表現していないと仮定して) であり、選択したインターフェイスを実装するテスト デリゲートを作成することです。Cookie は「取得」され、テストの目的には常に有効です。false にする必要がある場合は、別のテスト デリゲートを作成し、依存関係として提供します。
Mockolate は長い道のりを歩むことができますが、しっかりしたモックを構築することが重要です。
これは例です。あなたの質問に直接答えるものではありませんが、同じ問題のいくつかを表していると思うので、特定の状況を解決するのに役立つかもしれません.
このアプリで SQL 操作を処理するために、サード パーティのライブラリを使用しています。ライブラリには、SQLRunner と呼ばれるコア クラスがあります。たまたま、このクラスにはインターフェイスがありません (これにより、作業が少し簡単になります)。私には2つの選択肢がありました。
- ライブラリをフォークして変更し、インターフェイスを表現します。
- SQLRunner を独自のデリゲート クラスにラップします。
たまたま、私は両方を行いましたが、いくつかの理由から 2 番目のアプローチを好みます。サードパーティ ライブラリの API と機能を完全に定義できます。これは素晴らしいです。私は実際には API を変更していませんが、メソッドの命名方法が気に入らない場合は、変更してください。インターフェースも表現できました!Mockolate を使用してその使用法をモックしたかったので、私はもともとこれを行いました。また、より堅牢な機能を持ち、より明確な独自のテスト モックを作成するのにも役立ちました。だからここにサービスがあります:
public class SQLTaskService extends Actor implements ITaskService
{
[Inject]
public var sqlRunner:ISQLRunnerDelegate;
[Inject]
public var statusListModel:StatusListModel;
[Inject]
public var taskListModel:TaskListModel;
public function loadAllTasks():void
{
statusListModel.removeAllTasks();
sqlRunner.execute(LOAD_ALL_TASKS_SQL, null, loadAllTasksResultHandler, Task, databaseErrorHandler);
}
private function loadAllTasksResultHandler(result:SQLResult):void
{
for each(var task:Task in result.data)
{
var taskStatus:Status = statusListModel.getStatusFromId(task.statusId);
statusListModel.addTaskToStatus(task, taskStatus);
taskListModel.addTask(task);
}
}
public function loadTaskById(id:int):void
{
sqlRunner.execute(LOAD_TASK_SQL, {taskId:id}, loadTaskResultHandler, Task);
}
private function loadTaskResultHandler(result:SQLResult):void
{
var task:Task = result.data[0] as Task;
var taskStatus:Status = statusListModel.getStatusFromId(task.statusId);
task = taskListModel.updateTask(task);
statusListModel.addTaskToStatus(task, taskStatus);
}
public function save(task:Task):void
{
var params:Object = task.toParamObject();
sqlRunner.executeModify(Vector.<QueuedStatement>(
[new QueuedStatement(SAVE_TASK_SQL, params)]), saveTaskResultHandler, databaseErrorHandler);
}
private function saveTaskResultHandler(results:Vector.<SQLResult>):void
{
var result:SQLResult = results[0];
if (result.rowsAffected > 0)
{
var id:Number = result.lastInsertRowID;
loadTaskById(id);
}
}
public function deleteTask(task:Task):void
{
sqlRunner.executeModify(Vector.<QueuedStatement>([new QueuedStatement(DELETE_TASK_SQL, {taskId:task.taskId})]),
deleteTaskResult, databaseErrorHandler);
}
private function deleteTaskResult(results:Vector.<SQLResult>):void
{
//pass
}
private function databaseErrorHandler(error:SQLError):void
{
dispatch(new DatabaseErrorHandlerEvent(error.message));
}
[Embed(source="/assets/data/sql/tasks/SaveTask.sql", mimeType="application/octet-stream")]
private static const SaveTaskStatementText:Class;
public static const SAVE_TASK_SQL:String = new SaveTaskStatementText();
[Embed(source="/assets/data/sql/tasks/DeleteTask.sql", mimeType="application/octet-stream")]
private static const DeleteTaskStatementText:Class;
public static const DELETE_TASK_SQL:String = new DeleteTaskStatementText();
[Embed(source="/assets/data/sql/tasks/LoadTask.sql", mimeType="application/octet-stream")]
private static const LoadTaskStatementText:Class;
public static const LOAD_TASK_SQL:String = new LoadTaskStatementText();
[Embed(source="/assets/data/sql/tasks/LoadAllTasks.sql", mimeType="application/octet-stream")]
private static const LoadAllTasksStatementText:Class;
public static const LOAD_ALL_TASKS_SQL:String = new LoadAllTasksStatementText();
}
デリゲートの依存関係があることがわかります。
/**
* This is a delegate for the SQLRunner class that allows us to utilize an interface
* for the purposes of creating mocks. The actual SQLRunner class does not express
* an interface. This approach also allows us to encapsulate the usage of a 3rd party
* library into this single delegate.
*
* <p>An alternative would be to fork and modify the original library, which would
* definitely be a viable option and would help others in the future.</p>
*/
public class SQLRunnerDelegate implements ISQLRunnerDelegate
{
private var sqlRunner:SQLRunner;
public function SQLRunnerDelegate(dataBaseFile:File, maxPoolSize:int = 5)
{
sqlRunner = new SQLRunner(dataBaseFile, maxPoolSize);
}
public function get numConnections():int
{
return sqlRunner.numConnections;
}
public function get connectionErrorHandler():Function
{
return sqlRunner.connectionErrorHandler;
}
public function set connectionErrorHandler(value:Function):void
{
sqlRunner.connectionErrorHandler = value;
}
public function execute(sql:String, parameters:Object, handler:Function, itemClass:Class = null, errorHandler:Function = null):void
{
sqlRunner.execute(sql, parameters, handler, itemClass, errorHandler);
}
public function executeModify(statementBatch:Vector.<QueuedStatement>, resultHandler:Function, errorHandler:Function, progressHandler:Function = null):void
{
sqlRunner.executeModify(statementBatch, resultHandler, errorHandler, progressHandler);
}
public function close(resultHandler:Function, errorHandler:Function = null):void
{
sqlRunner.close(resultHandler, errorHandler);
}
}
それは本当にうまくいきました。現在、私のアプリケーションには、サードパーティ ライブラリとの小さな統合ポイントが 1 つあります。大勝利。また、ファイル システムへの依存関係やサードパーティ ライブラリのその他の奇妙な点に対処する必要なく、堅牢なモックを作成することもできます。
/**
* This is a more robust mock for the SQLRunnerDelegate to test for
* side effects that occur when methods are called on SQLTaskService
*/
public class MockTaskSQLRunnerDelegate extends MockSQLRunnerDelegateBase implements ISQLRunnerDelegate
{
public function execute(sql:String, parameters:Object, handler:Function, itemClass:Class = null, errorHandler:Function = null):void
{
lastStatementExecuted = sql;
allStatementsExecuted.push(lastStatementExecuted);
parametersSent = parameters;
switch (sql)
{
case SQLTaskService.LOAD_ALL_TASKS_SQL:
handler.call(null, loadTask());
break;
case SQLTaskService.LOAD_TASK_SQL:
handler.call(null, loadTask());
break;
default:
break;
}
}
private function loadTask():SQLResult
{
var task:Task = new Task();
var data:Array = [task];
var result:SQLResult = new SQLResult(data);
task.taskId = 1;
task.statusId = 1;
return result;
}
public function executeModify(statementBatch:Vector.<QueuedStatement>, resultHandler:Function, errorHandler:Function, progressHandler:Function = null):void
{
lastStatementExecuted = statementBatch[0].statementText;
allStatementsExecuted.push(lastStatementExecuted);
parametersSent = statementBatch[0].parameters;
switch (lastStatementExecuted)
{
case SQLTaskService.SAVE_TASK_SQL:
resultHandler.call(null, saveTask());
break;
}
}
private function saveTask():Vector.<SQLResult>
{
var task:Task = new Task();
var result:SQLResult = new SQLResult([task], 1, true, 1);
var results:Vector.<SQLResult> = new Vector.<SQLResult>();
task.taskId = task.statusId = 1;
results.push(result);
return results;
}
public function get numConnections():int
{
return 0;
}
public function get connectionErrorHandler():Function
{
return null;
}
public function set connectionErrorHandler(value:Function):void
{
}
public function close(resultHandler:Function, errorHandler:Function = null):void
{
}
}
そして、私は取引から素晴らしいテストスイートを手に入れました:
public class SqlTaskServiceTest
{
private var taskService:SQLTaskService;
[Before(async)]
public function setup():void
{
taskService = new SQLTaskService();
taskService.statusListModel = new StatusListModel();
taskService.taskListModel = new TaskListModel();
initializeModels();
prepareMockolates();
}
public function prepareMockolates():void
{
Async.proceedOnEvent(this, prepare(ISQLRunnerDelegate), Event.COMPLETE);
}
[Test]
public function loadAllTasks_executesSqlStatement_statementEqualsLoadAll():void
{
var runner:MockTaskSQLRunnerDelegate = new MockTaskSQLRunnerDelegate();
taskService.sqlRunner = runner;
taskService.loadAllTasks();
assertThat(runner.lastStatementExecuted, equalTo(SQLTaskService.LOAD_ALL_TASKS_SQL));
}
[Test]
public function loadAllTasks_clearsTasksFromStatusListModel_lengthIsEqualToZero():void
{
var status:Status = new Status();
var task:Task = new Task();
initializeModels(status, task);
taskService.sqlRunner = nice(ISQLRunnerDelegate);
taskService.loadAllTasks();
assertThat(status.tasks.length, equalTo(0))
}
[Test]
public function loadAllTasks_updatesTaskListModelWithLoadedTasks_collectionLengthIsOne():void
{
taskService.sqlRunner = new MockTaskSQLRunnerDelegate();
taskService.loadAllTasks();
assertThat(taskService.taskListModel.tasks.length, equalTo(1));
}
[Test]
public function loadAllTasks_updatesStatusWithTask_statusHasTasks():void
{
var status:Status = new Status();
initializeModels(status);
taskService.sqlRunner = new MockTaskSQLRunnerDelegate();
taskService.loadAllTasks();
assertThat(status.tasks.length, greaterThan(0));
}
[Test]
public function save_executesSqlStatement_statementEqualsSave():void
{
var task:Task = new Task();
var runner:MockTaskSQLRunnerDelegate = new MockTaskSQLRunnerDelegate();
taskService.sqlRunner = runner;
task.statusId = 1;
taskService.save(task);
assertThat(runner.allStatementsExecuted, hasItem(SQLTaskService.SAVE_TASK_SQL));
}
[Test]
public function save_taskIsLoadedAfterSave_statementEqualsLoad():void
{
var task:Task = new Task();
var runner:MockTaskSQLRunnerDelegate = new MockTaskSQLRunnerDelegate();
taskService.sqlRunner = runner;
task.statusId = 1;
taskService.save(task);
assertThat(runner.allStatementsExecuted, hasItem(SQLTaskService.LOAD_TASK_SQL));
}
[Test]
public function save_taskIsAddedToModelWhenNew_tasksLengthGreaterThanZero():void
{
var taskListModel:TaskListModel = taskService.taskListModel;
var task:Task = new Task();
taskListModel.reset();
taskService.sqlRunner = new MockTaskSQLRunnerDelegate();
task.statusId = 1;
task.taskId = 1;
taskService.save(task);
assertThat(taskListModel.tasks.length, equalTo(1));
}
[Test]
public function save_existingTaskInstanceIsUpdatedAfterSave_objectsAreStrictlyEqual():void
{
var taskListModel:TaskListModel = taskService.taskListModel;
var task:Task = new Task();
var updatedTask:Task;
taskListModel.addTask(task);
taskService.sqlRunner = new MockTaskSQLRunnerDelegate();
task.statusId = 1;
task.taskId = 1;
taskService.save(task);
updatedTask = taskListModel.getTaskById(task.taskId);
assertThat(updatedTask, strictlyEqualTo(task));
}
[Test]
public function loadTaskById_executesLoadStatement_statementEqualsLoad():void
{
var runner:MockTaskSQLRunnerDelegate = new MockTaskSQLRunnerDelegate();
taskService.sqlRunner = runner;
taskService.loadTaskById(1);
assertThat(runner.allStatementsExecuted, hasItem(SQLTaskService.LOAD_TASK_SQL));
}
[Test]
public function deleteTasks_executesDeleteStatement_statementEqualsDelete():void
{
var task:Task = new Task();
var runner:MockTaskSQLRunnerDelegate = new MockTaskSQLRunnerDelegate();
taskService.sqlRunner = runner;
taskService.deleteTask(task);
assertThat(runner.allStatementsExecuted, hasItem(SQLTaskService.DELETE_TASK_SQL));
}
private function initializeModels(status:Status = null, task:Task = null):void
{
var statusListModel:StatusListModel = taskService.statusListModel;
statusListModel.reset();
//if nothing was passed in we need to default to new objects
status ||= new Status();
task ||= new Task();
status.statusId = 1;
task.taskId = task.statusId = 1;
statusListModel.statuses.addItem(status);
statusListModel.addTaskToStatus(task, status);
}
}
補足: ここで指摘したいのは、非同期テストは 1 つだけではないということです。単体テストを非同期で実行する必要はほとんどありません。本当に必要なエッジ ケースもありますが、それらは例外です。