How can I test with jest an if statement within a reducer? - redux

I want to test this reducer but is pretty unclear to me how to run a test on a if statement.
case REMOVE_NOTIFICATION: {
if (state.getIn(['notification', 'message']) === action.message) {
return initialState;
}
return state;
}
case TICK_NOTIFICATION:
return state.setIn(['notification', 'timer'],
state.getIn(['notification', 'timer']) - 1);
default:
return state;

The if statement is an implementation detail and should not be tested. You want to test expected behaviour. You put an action on the reducer and expect a new state to be resolved. You want to test all the expected behaviours and not just one. This ensures you that changing an implementation detail does not break your test.

Related

Is it a bad idea to use something other than a string constant as an action type in redux?

For example, using a new String wrapper to prevent name clashes:
// actions/forms/types.js
export const SUBMIT = new String('SUBMIT');
// actions/tabs/types.js
export const SUBMIT = new String('SUBMIT');
Thus, when writing a reducer ...
// reducers/forms.js
import { SUBMIT as FORM_SUBMIT } from '../actions/forms/types.js'
import { SUBMIT as TAB_SUBMIT } from '../actions/tabs/types.js'
console.log(FORM_SUBMIT === TAB_SUBMIT); // false;
export default function (state, action) {
switch (action.type) {
case FORM_SUBMIT:
// correctly handle only FORM_SUBMIT, and not TAB_SUBMIT
default: return state;
}
}
Would there be any downside to this? Or is this a good idea? Can't find much against it or for it
That's probably not a good idea, on a couple levels.
Redux itself doesn't actually care what the value of action.type is - it only enforces that action.type is defined. From there, how your reducer logic makes decisions is up to you.
I actually wasn't familiar with the use of new String() to produce non-equal references, so I had to look that one up. Yes, that appears to produce valid differentiating comparisons, per this example:
const a = new String("abcd");
const b = new String("abcd");
const c = "abcd";
const d = "abcd";
function reducer(state, action) {
switch(action.type) {
case a: {
console.log("a");
break;
}
case b: {
console.log("b");
break;
}
case c: {
console.log("c");
break;
}
case d: {
console.log("d");
break;
}
}
}
reducer(undefined, {type : a});
reducer(undefined, {type : b});
reducer(undefined, {type : "abcd"});
HOWEVER... if you attempt to log these actions, and or view the action history in the Redux DevTools, you are not going to be able to tell the difference between them visually. The overall intent of Redux is to make it straightforward to track down what actions have been dispatched and when. As a developer, there's lots of ways you can subvert that intent, but you're just going to be making things harder on yourself.
As for things other than strings, such as numbers or Symbols, those are also bad ideas. Numbers aren't as readable in the action history as strings are, and Symbols are not serializable and will cause problems with debugging. (Also see the Redux FAQ entry on why actions should be serializable.
I'm currently working on a blog post that will discuss the actual technical limitations that Redux requires (and why), vs how Redux is intended to be used, vs how it's possible to use Redux. I'm currently aiming to have that post up early next week. If you're interested, keep an eye on my blog at http://blog.isquaredsoftware.com .

Is it possible to safely use Switch over FlowType union types (String Enums)?

In the following example, since I'm using matching over type of Message using the switch statement, I would like flow to recognise my incorrect case of 'ENUM_TYPO'. It currently doesn't.
type Message = 'BROADCAST_MESSAGE' | 'PRIVATE_MESSAGE';
const message: Message = 'BROADCAST_MESSAGE';
switch (message) {
case 'ENUM_TYPO':
// Do Broadcast
break;
default:
break;
}
As of v0.32.0, Flow does not complain about unreachable code, unless it's something like
// #flow
function foo() {
throw new Error();
return 123; // This will error
}.
However, consider the following code
// #flow
function foo(x: string): Object {
if (x === 123) {
return x;
}
return {};
}
Will currently will not error on this code. Flow does in fact notice that x === 123 will never be true. Inside the if block, Flow will refine the type of x to the empty type, since it doesn't believe that this code will ever be reached. That is why it doesn't complain about the return x statement.
One of the members of the Flow team is almost done with adding reachability analysis to Flow. Once this improvement lands (I'm guessing v0.34.0?), Flow will complain when it sees a conditional that it thinks will always fail. This will help you with your example, since switch statement cases are basically strict equality checks.

Test an async PipeTransform

Context
I have a basic PipeTransform, expect the fact that it is async. Why? because I have my own i18n service (because of parsing, pluralization and other constraints, I did my own) and it returns a Promise<string>:
#Pipe({
name: "i18n",
pure: false
})
export class I18nPipe implements PipeTransform {
private done = false;
constructor(private i18n:I18n) {
}
value:string;
transform(value:string, args:I18nPipeArgs):string {
if(this.done){
return this.value;
}
if (args.plural) {
this.i18n.getPlural(args.key, args.plural, value, args.variables, args.domain).then((res) => {
this.value = res;
this.done = true;
});
}
this.i18n.get(args.key, value, args.variables, args.domain).then((res) => {
this.done = true;
this.value = res;
});
return this.value;
}
}
This pipe works well, because the only delayed call is the very first one (the I18nService uses lazy loading, it loads JSON data only if the key is not found, so basically, the first call will be delayed, the other ones are instant but still async).
Problem
I can't figure out how to test this pipe using Jasmine, since it is working inside a component I know it works, but the goal here is to get this fully tested using jasmine, this way I can add it to a CI routine.
The above test:
describe("Pipe test", () => {
it("can call I18n.get.", async(inject([I18n], (i18n:I18n) => {
let pipe = new I18nPipe(i18n);
expect(pipe.transform("nope", {key: 'test', domain: 'test domain'})).toBe("test value");
})));
});
Fails because since the result given by the I18nService is async, the returned value is undefined in a sync logic.
I18n Pipe test can call I18n.get. FAILED
Expected undefined to be 'test value'.
EDIT: One way to do it would be to use setTimeout but I want a prettier solution, to avoid adding setTimeout(myAssertion, 100) everywhere.
Use fakeAsync from #angular/core/testing. It allows you to call tick(), which will wait for all currently queued asynchronous tasks to complete before continuing. This gives the illusion of the actions being synchronous. Right after the call to tick() we can write our expectations.
import { fakeAsync, tick } from '#angular/core/testing';
it("can call I18n.get.", fakeAsync(inject([I18n], (i18n:I18n) => {
let pipe = new I18nPipe(i18n);
let result = pipe.transform("nope", {key: 'test', domain: 'test domain'});
tick();
expect(result).toBe("test value");
})));
So when should we use fakeAsync and when should we use async? This is the rule of thumb that I go by (most of the time). When we are making asynchronous calls inside the test, this is when we should use async. async allows to test to continue until all asynchronous calls are complete. For example
it('..', async(() => {
let service = new Servce();
service.doSomething().then(result => {
expect(result).toBe('hello');
});
});
In a non async test, the expectation would never occur, as the test would complete before the asynchronous resolution of the promise. With the call to async, the test gets wrapped in a zone, which keeps track of all asynchronous tasks, and waits for them to complete.
Use fakeAsync when the asynchronous behavior is outside the control of the test (like in your case is going on in the pipe). Here we can force/wait for it to complete with the call to tick(). tick can also be passed a millisecond delay to allow more time to pass if needed.
Another option is to mock the service and make it synchronous, as mentioned in this post. When unit testing, if your components in test are dependent on heavy logic in the service, then the component in test is at the mercy of that service working correctly, which kinda defeats the purpose of a "unit" test. Mocking makes sense in a lot of cases.

Compare previous state

In issue 303 in the Redux repo, Dan Abramov gives an example of a function that can wrap a store’s subscribe method in order to pass the previous state to the subscriber.
function observeStore(store, select, onChange) {
let currentState;
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState);
}
}
let unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
For some reason, this doesn't work for me. The first time my onChange handler is called currentState is undefined, as I’d expect. However, on each subsequent state change, the properties of currentState are equivalent in value (==) to those of nextState. The two state objects aren’t the same object, though because nextState === currentState evaluates to false.
I’m likely missing something really obvious. How do I capture the previous state in a closure?
The problem is that my reducer wasn’t actually creating a new instance of the state, i.e. I was mutating the state in place, rather than returning a new copy. My reducer code was something like:
const newState = Object.assign({}, state);
newState.my.prop = …;
return newState;
However, Object.assign and ES2015 destructuring assignment only do shallow copies, meaning they only affect one level deep. In my app, the state I was updating was three levels deep. I used a quick, dirty, ugly hack in my reducer to test my theory:
const newState = JSON.parse(JSON.stringify(state)); // <-- Yuck!
newState.my.prop = …;
return newState;
Don’t try that at home. This was my first clue, though. A proper implementation would use something like Facebook’s Immutable library to ensure each mutation results in a fresh clone.

Complex operation with BaconJS (FRP)

I'm trying to do this relatively complex operation in BaconJs.
Basically, the idea is keep trying each check until you have a 'pass' status or they all fail. The catch is that 'pending' statuses have a list of Observables (built from jquery ajax requests) that will resolve the check. For performance reasons, you need to try each Observable in order until either they all pass or one fails.
Here's the full pseudo algorithm:
Go thru each check. A check contains an id and status = fail/pass/pending. If pending, it contains a list of observables.
If status = pass, then return the id (you're done!)
if status = fail, then try the next check
if status = pending
try each observable in order
if observable result is 'false', then try the next check
if reach end of observable list and result is 'true', then return the id (you're done!)
Here's the Bacon code. It doesn't work when the Observables are Ajax requests.
Basically, what happens is that it skips over pending checks....it doesn't wait for the ajax calls to return. If I put a log() right before the filter(), it doesn't log pending requests:
Bacon.fromArray(checks)
.flatMap(function(check) {
return check.status === 'pass' ? check.id :
check.status === 'fail' ? null :
Bacon.fromArray(check.observables)
.flatMap(function(obs) { return obs; })
.takeWhile(function(obsResult) { return obsResult; })
.last()
.map(function(obsResult) { return obsResult ? check.id : null; });
})
.filter(function(contextId) { return contextId !== null; })
.first();
UPDATE: the code works when the checks look like this: [fail, fail, pending]. But it doesn't work when the checks look like this: [fail, pending, pass]
I am more familiar with RxJS than Bacon, but I would say the reason you aren't seeing the desired behavior is because flatMap waits for no man.
It passes [fail, pending, pass] in quick succession, fail returns null and is filtered out. pending kicks off an observable, and then receives pass which immediately returns check.id (Bacon may be different, but in RxJS flatMap won't accept a single value return). The check.id goes through filter and hits first at which point it completes and it just cancels the subscription to the ajax request.
A quick fix would probably be to use concatMap rather than flatMap.
In RxJS though I would refactor this to be (Disclaimer untested):
Rx.Observable.fromArray(checks)
//Process each check in order
.concatMap(function(check) {
var sources = {
//If we pass then we are done
'pass' : Rx.Observable.just({id : check.id, done : true}),
//If we fail keep trying
'fail' : Rx.Observable.just({done : false}),
'pending' : Rx.Observable.defer(function(){ return check.observables;})
.concatAll()
.every()
.map(function(x) {
return x ? {done : true, id : check.id} :
{done : false};
})
};
return Rx.Observable.case(function() { return check.status; }, sources);
})
//Take the first value that is done
.first(function(x) { return x.done; })
.pluck('id');
What the above does is:
Concatenate all of the checks
Use the case operator to propagate instead of nested ternaries.
Fail or pass fast
If pending create a flattened observable out of check.observables, if they are all true then we are done, otherwise continue to the next one
Use the predicate value of first to get the first value returned that is done
[Optionally] strip out the value that we care about.
I agree with #paulpdaniels Rx-based answer. The problem seems to be that when using flatMap, Bacon.js won't wait for your first "check-stream" to complete before launching a new one. Just replace flatMap with flatMapConcat.
Thanks to #raimohanska and #paulpdaniels. The answer is to use #flatMapConcat. This turns what is basically a list of async calls done in parallel into a sequence of calls done in order (and note that the last "check" is programmed to always pass so that this always outputs something):
Bacon.fromArray(checks)
.flatMapConcat(function(check) {
var result = check();
switch(result.status) {
case 'pass' :
case 'fail' :
return result;
case 'pending' :
return Bacon.fromArray(result.observables)
.flatMapConcat(function(obs) { return obs; })
.takeWhile(function(obsResult) { return obsResult.result; })
.last()
.map(function (obsResult) { return obsResult ? {id: result.id, status: 'pass'} : {status: 'fail'}; });
}
})
.filter(function(result) { return result.status === 'pass'; })
.first()
.map('.id');

Resources