Unit testing JavaScript with Promises and Jasmine

Promises are a nice solution to write readable asynchronous code in JavaScript. If you understand Spanish, I highly recommend this great talk on Promises by Enrique Amodeo.

But, is the code with promises easy to unit test? It's not very difficult, you have to take into account the calls are asynchronous even if the production code executes instantly.

Here is some code I've written using when.js library:

  1.  
  2. function TicketSalesService(){
  3. var self = this;
  4. // ... some other code over here...
  5. this.buyTickets = function(quantity) {
  6. purchaseOrder.quantity = quantity;
  7. // THIS IS THE CODE WITH PROMISES:
  8. this.server.isUserRegistered(user)
  9. .then(tryToBuyTickets)
  10. .then(informSubscribersAboutPurchaseResult)
  11. .otherwise(function (err) {
  12. helpers.registerPromiseError(self, err);
  13. });
  14. };
  15. var tryToBuyTickets = function (response) {
  16. if (!response.registered){
  17. self.onRegistrationRequired(); // trigger event
  18. throw helpers.endOfPromiseChain; // stop the chain
  19. }
  20. return self.server.buyTickets(purchaseOrder);
  21. };
  22. var informSubscribersAboutPurchaseResult = function (response) {
  23. if (response.success)
  24. self.onPurchaseSuccess();
  25. else
  26. self.onPurchaseFailure(response.message);
  27. };
  28. this.onRegistrationRequired = function() {/* event */};
  29. this.onPurchaseSuccess = function() {/* event */};
  30. this.onPurchaseFailure = function() {/* event */};
  31. // ... some other code over here....
  32. };
  33.  

In this code there is a helper namespace providing a function and a constant:

  1.  
  2. helpers.endOfPromiseChain; // this is just a constant, a string.
  3. // And this is a function to ease testing:
  4. helpers.registerPromiseError = function (target, err) {
  5. if (err != helpers.endOfPromiseChain){
  6. target.errorInPromise = err.toString();
  7. }
  8. };
  9.  

The "server" dependency in the TicketSalesService is just a jQuery ajax wrapper. There is a rule in TDD that says, "do not mock artifacts you don't own". What I do is wrap up jQuery.ajax so that I can easily stub out the server response and also change jQuery for other library if I needed to.

  1.  
  2. Service.prototype.isUserRegistered = function (user) {
  3. var deferred = when.defer();
  4. // wrapping jQuery aja:
  5. this.requestData("theUrl/goes/over/here",
  6. { data: user },
  7. function(data) { // jQuery.ajax success invokes this
  8. deferred.resolve(data);
  9. });
  10. // TODO. call deferred.reject(); on error
  11. return deferred.promise;
  12. };
  13.  

What about the tests? I am using Jasmine as the testing framework. I test-drove the code above and these are the resulting tests:

  1.  
  2. it("triggers event when server responds that the user is not registered",
  3. function () {
  4. stubServerResponse(salesService.server, { registered: false });
  5. var promiseSpy = spyReturningPromise(salesService,
  6. "onRegistrationRequired");
  7.  
  8. salesService.buyTickets(5);
  9.  
  10. assertAsyncExpects(promiseSpy, salesService);
  11. });
  12.  
  13. it("tries to buy when server responds that the user is registered",
  14. function () {
  15. stubServerResponse(salesService.server, { registered: true });
  16. var promiseSpy = spyReturningPromise(salesService.server,
  17. "buyTickets");
  18.  
  19. salesService.buyTickets(5);
  20.  
  21. assertAsyncExpects(promiseSpy, salesService);
  22. });
  23.  
  24. it("triggers event when tickets are purchased",
  25. function () {
  26. stubServerResponse(salesService.server,
  27. {success: true, registered: true});
  28. var promiseSpy = spyReturningPromise(salesService,
  29. "onPurchaseSuccess");
  30.  
  31. salesService.buyTickets(5);
  32.  
  33. assertAsyncExpects(promiseSpy, salesService);
  34. });
  35.  
  36. it("triggers event when prescriptions could not be purchased",
  37. function () {
  38. stubServerResponse(salesService.server,
  39. {success: false, registered: true, message: 'fatal'});
  40. var promiseSpy = spyReturningPromise(salesService,
  41. "onPurchaseFailure");
  42.  
  43. salesService.buyTickets(5);
  44.  
  45. assertAsyncExpects(promiseSpy, salesService);
  46. });
  47.  

My code is using DOM Level 0 event handling. You can read more about event driven design in this former post.
The tests are very clean if you are familiar with Jasmine spies.
The method "stubServerResponse" replaces the function "requestData" in my "server" object to simulate data coming from the server.
The other helpers are here:

  1.  
  2. var assertAsyncExpects =
  3. function(promiseSpy, target, additionalExpectation) {
  4. waitsFor(function () {
  5. return promiseSpy.called ||
  6. target.errorInPromise; }, 50
  7. );
  8. runs(function () {
  9. // this tells me if there was any unhandled exception:
  10. expect(target.errorInPromise).not.toBeDefined();
  11. // this asks the spy if everything was as expected:
  12. expect(promiseSpy.target[promiseSpy.methodName]
  13. ).toHaveBeenCalled();
  14. // optional expectations:
  15. if (additionalExpectation)
  16. additionalExpectation();
  17. });
  18. };
  19.  
  20. var spyReturningPromise =
  21. function(target, methodName) {
  22. var spyObj = {called: false,
  23. target: target,
  24. methodName: methodName};
  25. spyOn(target, methodName).andCallFake(function () {
  26. spyObj.called = true;
  27. return when.defer().promise;
  28. });
  29. return spyObj;
  30. }
  31.  

These two are basically wrappers around Jasmine to remove noise from my tests. The funcionts "waitsFor", "runs" and "spyOn", belong to Jasmine. The first two deal with asynchronous execution whereas the third one creates a test double, a spy object.

What are the tricky parts?

When there is an exception inside the "then" or "otherwise" functions, it is captured by the promise and the test doesn't know anything about it. So when the test fails, it might not be for an obvious reason, and I want my unit tests to tell me where the problem is very fast. So I create a property in the object containing the "then" methods, called "errorInPromise" that I can check later in my tests. I add the "otherwise" handler at the end of the "then" blocks to ensure any exception thrown within them is captured and can be read in the tests.

What do you do to unit test your "promising code"?

Enjoyed reading this post?
Subscribe to the RSS feed and have all new posts delivered straight to you.
  • Kevin

    Thanks for this. I’m using jQuery promises and used this as a starting point. I still don’t have it all the way working, but I submitted a request to the jasmine-jquery project (https://github.com/velesin/jasmine-jquery/issues/121).

  • carlosble

    Hi Kevin! I am glad you fond this useful. Let me know if I can help you with anything 🙂

  • Pingback: Unit testing JavaScript with Promises and Jasmine « El blog de Carlos Ble | test()

  • JD

    To handle exceptions in Promises, you can also embed tests in the success/error callback, which is much cleaner.
    Promise.reject().then(function(){fail(‘We were expecting an error’)}, function(e){expect(e).toEqual(jasmine.any(Error))})