Wednesday, July 17, 2013

Mock HttpRequests for Testing Dart Servers


I have not done much server-side testing in Dart yet. Tonight seems like as good a time as any to kick the tires.

Unlike client-side testing in Dart, there are a number of solid testing options available to me. The most obvious is to actually run the server and verify that the responses returned are what I expect. The other option is to convert the server.dart file into a Dart library so that the individual functions and classes can be probed directly. I will try the latter tonight since it just seems more fun.

To import a library for test, the thing being imported needs to be a… library. To convert test_server.dart into a library, I need only add the library statement to the top of the file:
library plumbur_kruk;
// imports and variable declarations...
main() {
  HttpServer.bind('127.0.0.1', 31337).then((app) {
    app.listen((HttpRequest req) {
      log(req);
      // handle requests here...
      notFoundResponse(req);
    });
  });
}
The library name does not matter as long as it is alphanumeric and underscores.

With that, I write a test that imports my new library:
library plumbur_kruk_test;

import 'package:unittest/unittest.dart';
import 'dart:io';

import 'test_server.dart' as PlumburKruk;

main(){
  group("Core Server", (){
    test("non-existent resource results in a 404", (){
      var req = new HttpRequest();
    });
  });
}
In the main() entry point, I have written my first test that verifies that I can get a 404 response back from the non-existent request handling code. Actually, there is no real test there—I am just wondering if I can create a new HttpRequest object.

In fact, I cannot:
➜  plumpbur-kruk git:(master) ✗ dart test/server_test.dart
unittest-suite-wait-for-done
ERROR: Core Server non-existent resource results in a 404
  Test failed: Caught Cannot instantiate abstract class HttpRequest: url 'file:///home/chris/repos/plumpbur-kruk/test/server_test.dart' line 15
  core_server_tests.<anonymous closure>.<anonymous closure>   file:///home/chris/repos/plumpbur-kruk/test/server_test.dart 15:21
...
Bah! I am on the fence, but I do not think that I like this. It bothers me a little that I cannot directly create this object. It bothers me more that I can create an HttpRequest instance on the client side.

In this case, I am getting the HttpRequest class from the dart:io library. When I code Dart in the browser, a different HttpRequest class comes from the dart:html library. HttpRequest from dart:io is meant to handle HTTP requests in a server environment. HttpRequest from dart:html is a replacement for the venerable XMLHttpRequest object for performing so-called AJAX operations.

I cannot create instances of HttpRequest from dart:io—only a server's listen stream can do that. It is still possible to make HTTP requests from the command-line in Dart—the HttpClient class does that. Naturally, it does not have an HttpRequest object for me to use, preferring to expose them in futures.

Basically, I am stuck. I cannot create an HttpRequest object for my server test. But I can create something the quacks like an HttpRequest duck. Rather than building a fake class manually, I am going to give the mock library a go. I may be trying one too many new things at this point, but what the hey?

The mock library is already included in the unittest library, so there is no need to update my pubspec.yaml. It already depends on unittest:
name: plumbur_kruk
#...
dependencies:
  unittest: any
Since I already have unittest there is also no need to install with Dart Pub.

I should be good to create a mock instance of HttpRequest. I update my test to import the mock library, then I create a MockHttpRequest as implementing HttpRequest, and finally create an instance of this mock class:
library plumbur_kruk_test;

import 'package:unittest/unittest.dart';
import 'package:unittest/mock.dart';
import 'dart:io';

import 'package:plumbur_kruk/server.dart' as PlumburKruk;

class MockHttpRequest extends Mock implements HttpRequest {}

main(){
  group("Core Server", (){
    test("non-existent resource results in a 404", (){
      var req = new MockHttpRequest();
    });
  });
}
And that works. Or at least it compiles and successfully creates an instance of MockHttpRequest:
➜  plumpbur-kruk git:(master) ✗ dart test/server_test.dart
unittest-suite-wait-for-done
PASS: Core Server non-existent resource results in a 404

All 1 tests passed.
unittest-suite-success
Of course, I am not really testing anything. I am going to need a second mock class to test the response—a MockHttpResponse class:
class MockHttpRequest extends Mock implements HttpRequest {}
class MockHttpResponse extends Mock implements HttpResponse {}
To make use of this, my test gets a little more complicated. I start by creating a mock HttpResponse instance and a mock HttpRequest instance:
      var response = new MockHttpResponse();
      var req = new MockHttpRequest()
        ..when(callsTo('get response')).alwaysReturn(response);
In addition to creating the mock request object, I give it some behavior. I tell it that, when it sees calls to the response getter, it should always return my mock HTTP response object. This syntax feels a little awkward, but it is fairly readable.

Next, I call the noResponseFound() method in my actual test server with the mock request object:
      PlumburKruk.notFoundResponse(req);
Last, I need to check the mock request object's “logs” to see how many times the statusCode setter was told to assign a 404 value:
      response.
        getLogs(callsTo('set statusCode', 404)).
        verify(happenedOnce);
Here, I am verifying that, according the logs that the mock hold, the setter was called once with a value of 404.

In the actual server method, it really is only set once:
notFoundResponse(req) {
  HttpResponse res = req.response;
  res.statusCode = HttpStatus.NOT_FOUND;
  res.close();
}
So, if I have done this correctly, my test should pass:
➜  plumpbur-kruk git:(master) ✗ dart test/server_test.dart
unittest-suite-wait-for-done
PASS: Core Server non-existent resource results in a 404

All 1 tests passed.
unittest-suite-success
Yay! That seems legit. To be sure, I temporarily break the notFoundResponse() function to set a 200 status code:
notFoundResponse(req) {
  HttpResponse res = req.response;
  res.statusCode = HttpStatus.OK;
  res.close();
}
And, that does indeed cause a test failure:
➜  plumpbur-kruk git:(master) ✗ dart test/server_test.dart
unittest-suite-wait-for-done
FAIL: Core Server non-existent resource results in a 404
  Expected set statusCode(<404>) to be called 1 times
       but: was called 0 times. 
  LogEntryList.verify                               package:unittest/mock.dart 568:11
  main.<anonymous closure>.<anonymous closure>      file:///home/chris/repos/plumpbur-kruk/test/server_test.dart 23:15
...
I am pretty excited to finally have put the mock library in Dart to some good use—all the more so because the resultant test is pretty solid. It would still make some sense to contrast this approach with a real HTTP client and server, which I will likely tackle tomorrow.


Day #815

No comments:

Post a Comment