2

A co-worker ran into the problem that a computed he wanted to test was not returning the expected output. This happens because we want to stub other computeds (which again are dependent on other computeds). After stubbing there are 0 observables left in the computed and the computed keeps returning the cached result.

How can we force a computed to re-evaluate which no no longer has the original observables inside?

const ViewModel = function() {
  this.otherComputed = ko.computed(() => true);
  this.computedUnderTest = ko.computed(() => this.otherComputed());
};

const vm = new ViewModel();

function expect(expected) {
  console.log(vm.computedUnderTest() === expected);
}

// Init
expect(true);

// Stub dependent computed
vm.otherComputed = () => false;

// Computed no longer receives updates :(
expect(false);

// Can we force re-evaluation?
// vm.computedUnderTest.forceReEval()
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
Jonathan
  • 8,771
  • 4
  • 41
  • 78
  • A computed will re-evaluate if it finds that any values inside have changed. It can only really do that if the results are dynamic especially observables, since it can be notified of changes. By "stubbing" the computed you've *removed* it - the tested computed subscribes to the actual *object* belonging to the property `otherComputed` and doing `otherComputed = /* whatever*/` does not notify of changes because the other *object* is still around. KO can't really check if you've changed what a property points to. – VLAZ Nov 06 '18 at 11:04
  • Yes, that is exactly what we are running into but is there a way to force KO to re-evaluate? – Jonathan Nov 06 '18 at 14:34
  • Well, sort of. You can add a fake dependency by making the computed also take into account another observable but...just discard the result. That way, if you update the other observable, you will re-evaluate the computed. It's a very ugly hack but I had to use it once because of weirdness. You can also re-engineer it so you don't stub the observable by overriding the property. I never had the time to look into doing that generically (the fake dependency was a better option) but perhaps what @user3297291 wrote can be used. I'll take a look later. – VLAZ Nov 06 '18 at 15:31

1 Answers1

3

The only solution I can think of that doesn't involve changing the code of ViewModel, is to stub ko.computed first...

In the example below I replace ko.computed by an extended version. The extension exposes a property, .stub, that allows you to write a custom function. When this function is set, the computed will re-evaluate using the provided logic.

In your test file, you'd need to be able to replace the global reference to ko.computed in your preparation code, before instantiating a ViewModel instance.

// Extender that allows us to change a computed's main value getter
// method *after* creation
ko.extenders.canBeStubbed = (target, value) => {
  if (!value) return target;
  
  const stub = ko.observable(null);
  const comp =  ko.pureComputed(() => {
    const fn = stub();
    
    return fn ? fn() : target();
  });
  
  comp.stub = stub;
  
  return comp;
}

// Mess with the default to ensure we always extend
const { computed } = ko;
ko.computed = (...args) => 
  computed(...args).extend({ canBeStubbed: true });

// Create the view model with changed computed refs
const ViewModel = function() {
  this.otherComputed = ko.computed(() => true);
  this.computedUnderTest = ko.computed(() => this.otherComputed());
};

const vm = new ViewModel();

function expect(expected) {
  console.log("Test succeeded:", vm.computedUnderTest() === expected);
}


expect(true);

// Replace the `otherComputed`'s code by another function
vm.otherComputed.stub(() => false);

expect(false);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

In my own projects, I tend to use a completely different approach for testing my computeds which focuses on separating the logic from the dependencies. Let me know if the example above doesn't work for you. (I'm not going to write another answer if this already satisfies your needs)

user3297291
  • 22,592
  • 4
  • 29
  • 45