How do I test services with cookie based sessions in Flex/AS3
First time posting here. Thanks for taking the time to review the issue.
As the title indicates the question is in regards to the service layer of a flex application. Specifically in a test case class. To call the services the user must first authenticate through an html/CF based page. Once that's done and the authentication has occurred the user is directed to the html page with the flex application embedded in. Once authenticated the server side CF code writes a cookie to t开发者_如何学JAVAhe users computer. This cookie is then read by the flex app and is required for the flex app to make calls to the CF services.
The question is: how should this be handled in a test case class for the service class in the flex app? The following steps need to basically take place: use some account data to hit the server and authenticate, the cookie then needs to be written (which it will already), then the test case needs to pick up the session id from the cookie and pass it to the service call within the test case. That just all seems like too much for a test case class.
So, how is this type of thing usually handled. From what I gather it's typical for web service calls to require a session id like this.
Any thoughts, input, suggestions, etc. are greatly appreciated.
Thanks for your time and any information you can provide.
Sean
The cookie and the actual service calls would be tested in integration tests. Yoyou are correct in thinking that these bits are innappropriate for a unit test.
What I like to do is great a delegate (assuming your integration points don't already express an interface) and create test delegates that implement the interface I choose. The cookie would be "retrieved" and it would ALWAYS be valid for the purpose of your test. If you need it to be false, make another test delegate and supply it as a dependency.
Mockolate can go a long way, but building out solid mocks is the key.
So this is an example. It doesn't answer your question directly, but I think it represents some of the same problems, and thus might help you solve you specific set of circumstance.
I am using a third party library to handle the SQL operations in this app. The library has a core class called SQLRunner. This class, as it happens, doesn't have an interface (which would make things a bit easier). I had two choices.
- Fork and modify the library to express interfaces.
- Wrap the SQLRunner in my own delegate class.
As it happens, I did both, but prefer the 2nd approach for a number of reasons. I am able to fully define the API and functionality of the 3rd party library. This is awesome. I didn't actually change the API, but if you don't like how it names methods... no worries, change it up. It also allowed me to express an interface! I originally did this because I wanted to use Mockolate to mock its usage. It also came in handy for creating my own testing mocks that had more robust capabilities and were just clearer. So here is the service:
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();
}
you can see it has the delegate dependency:
/**
* 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);
}
}
That worked out really well. Now my application has this one tiny integration point with a third party library. Big win. I also get to make robust mocks without having to deal with dependencies on the file system or any other oddities of the third party library:
/**
* 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
{
}
}
And I got a nice test suite out of the deal:
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);
}
}
Side Note: Something I want to point out here is that there is not one single asynchronous test. There is rarely a need for running unit tests async. There are edge cases where you might really need to, but they are the exception.
精彩评论