Proper way to handler timers w/ Ember Testing


#1

After @cavneb’s talk at EmberConf, I decided to ditch our old testing framework (Dominator and other yucky crap) and port over to Ember.Testing. It went very smoothly, but I’m left with one question, and that is who to handle continuous timers.

Back story: We have one controller that uses a 30 second timer by calling Ember.run.later. This timer is used to compute the state of data that is time sensitive and cannot otherwise be observed.

The problem this causes is that wait used by Ember.Testing waits until all timers have been executed before moving on. Which will never happen in most of my routes due to this polling timer. So this is an open question on what the best approach is here. I’m open to anything, just interested to know if there is a right way or wrong way to handle this problem.

My question is very similar to: Testing and scheduled timers in run loop which went unanswered, but I felt my scenario was different enough to ask separately.


#2

@workmanw can you provide a jsbin or an example? I know that sinon.js has very solid time manipulation capabilities that I have used in testing Ember apps. I’m not sure that’s what you are looking for though.


#3

@cavneb Here you go: http://jsbin.com/vokogohi/3/edit

If you notice, the test will just stay “stalled” for 2 minutes because of the timer that is continually running (or until you click “Stop Timer”). The reason for this is because I’m using Ember.run.later for my timing needs, instead of setTimeout. The wait function in the test suite waits until all Ember timers have completed before continuing (ember-testing/lib/helpers.js#L154).

So I could use setTimeout and this wouldn’t be a problem. Or I could even make my controller aware of App.testing and not actually start the timer. Or I could try to inject something into that controller to prevent it from starting the timer. I guess this is more of an open ended question, than looking for one specific answer.


#4

There are a couple of circumstances in my app where I need to use setTimeout and I’ve had success testing them by stubbing it globally with a function that calls the original setTimeout with zero delay (using Sinon). The same approach should work with Ember.run.later, either by stubbing it directly, or stubbing setTimeout like I did since backburner ends up calling it anyway.

The other alternative is to make that delay value an app-level config setting that the testing harness can override. This approach has the added benefit of being able to tweak subjective settings later without code change.


#5

I’m sorry, I think I’ve been a little unclear.

My question isn’t as much about how to test the timer and it’s functionality. That timer is one minor thing that alerts the user to a potential timeout.

My question was really more about preventing the Ember.run.later timer from stalling out all of my tests. Clearly it can’t be used with the Ember.Test suite. I was just looking for thoughts on a potential work around. But I think I’m going to go with using setTimeout instead.


#6

I have the same issue as @workmanw. Here is a bin of what happens http://jsbin.com/filavupi/3/edit.

I need to recursively call the Ember.run.later in order to do the polling, and thus the wait timer will never exit.

I think the desired behavior in most cases for acceptance tests is to let them continue to run and maybe have a “waitForTimers” helper that can be used when timers need to be tested.


#7

@workmanw, I think I finally understand the problem you were having, because I have it now too :wink:

In my case, I’m displaying an alert that is the ultimate result of an AJAX response, and the alert has an auto-dismiss timer that uses Ember.run.later. Since the wait helper blocks on XHRs and the run loop, the tests won’t resume until the timer completes, stalling them out. I’m curious what you ended up doing (setTimeout or otherwise), since I’m not keen on making an app-level setting just for disabling/changing alert auto-dismissing…


#8

I have the same problem as @andremalan and I couldn’t find a solution yet. Someone proposed calling Ember.run.cancelTimers (not in the public API but in the code https://github.com/emberjs/ember.js/blob/v1.7.0/packages/ember-metal/lib/run_loop.js#L263) but that doesn’t work well for integration tests. Also, I’m a bit wary of disabling all timers globally. There are some timers I would like to keep active during testing. I’m curious what other people came up with to solve this problem. My solution up until now was to move the tests for the route with infinite timers to the Rails world in Capybara but this is clearly not satisfying.


#9

@YoranBrondsema @slindberg

I decided that it was actually a good idea for me to use Ember.run.later because it would potentially allow me to avoid some race conditions. I.E. if the setTime fired after the test suite had tour down, etc. So forcing the app to wait until the time has executed was a healthy thing [for me]. My real complaint was simply the time it took.

So I ended up creating a static config object that was registered with the App.register and injected it into my views and controllers. This config contained all the desired timer values (among other things). So then in my test setup I would substitute that config object with very short timer values (1ms). This approach is a little bit over the top, I really could have used global variables, but it felt the most inline with Ember’s testing pattern.

// .....................................................
// App Initializer
var globalSettings = Ember.Object.create({   
   bannerTimeout: 2500 
});

// Test suite will inject it's own
if(!App.testing) {
  App.register('globals:settings', globalSettings); 
}
App.inject('view', 'globalSettings', 'globals:settings');
App.inject('controller', 'globalSettings', 'globals:settings');


// .....................................................
// View definition
App.MyBannerView = Ember.View.extend({
   /* ... */
  setHideTime: function() {
     Ember.run.later(this, this.hide, this.get('globalSettings.bannerTimeout'));
  }.on('show')
});

// .....................................................
// Test suite example
function startApp() {
   /* ... */
  var globalTestSettings = Ember.Object.create({   
    bannerTimeout: 1 
  });
  
  Ember.run(function () {
     App = Application.create();
     App.register('globals:settings', globalTestSettings); 
     App.setupForTesting();
     App.injectTestHelpers();
   });
   /* ... */
}

Weird behaviour in test runner after update to 1.10 + 0.1.15 combo
#10

I think your approach is good and it looks clean to me thanks to dependency injection. I’m still not sure how to resolve testing the infinite Ember.run.later though… Will keep you posted when I resolve it.


#11

Has anyone figured out how to resolve testing with infinite Ember.run.later? we are using similar timer and the tests hang because the runloop never executes actions on the integration test.


#12

I came up with a solution that has worked pretty well for me, it may not work for everyone, but for times when you don’t need to test the Run.later loop, you can just wrap the run loop in a conditional check for the current environment. If the environment is ‘test’, just don’t fire the run.later loop.


#13

Reviving this hurtful topic, I would like to offer what I’ve come up with, as well as ask for reviews.

I’ve obtained an equivalent of Ember.run.later that currently covers the “infinite Ember.run.later loop” and is:

  • fully Promise-based
  • test friendly
  • cancelable (eventual cancelation, not immediate cancelation)
  • Uses Ember.run.later internally, but doesn’t expose anything from it

For the moment, it appears a bit mixed up with a (small) service, but I intend to make an addon so it is available as a Ember utility. It is certainly not perfect, but I’d like to know how/if it adapts to your use cases.

Here’s for you to review: https://github.com/xcambar/ember-cli-self-update/blob/master/addon/services/selfupdate.js


#14

Thanks, this is still a good solution.

For those who find this, in Ember 2.16.0,

App.register('globals:settings', globalSettings);

should be

App.register('globals:settings', globalSettings, { instantiate: false });

otherwise, this works like a charm.