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>
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)