Note: This is not a real App but a distilled version of the problem.
I have a simple application fake Todo that uses @computed
to get the value of TodoItemComputed.isSelected
:
import { computed, observable, action } from 'mobx';
export class TodoItemComputed {
readonly id: string;
readonly list: TodoListComputed;
constructor(id: string, list: TodoListComputed) {
this.id = id;
this.list = list;
}
// ** This is the attribute we are interested in and we want to test!
@computed
get isSelected() {
return this.id === this.list.selectedId;
}
}
export class TodoListComputed {
@observable.shallow
private serverData: string[] = [];
@observable
selectedId = '';
@action
setServerData(data: string[]) {
this.serverData = data;
}
@action
setSelected(id: string) {
this.selectedId = id;
}
@computed
get todoItems() {
return this.serverData.map(d => new TodoItemComputed(d, this));
}
}
Testing TodoItemComputed.isSelected
I could do something like this:
import { TodoListComputed } from '../TodoExample';
// Using jesthere, but the test framework should not matter...
describe('isSelected', function() {
it('should have no Item selected initially', function() {
const todoList = new TodoListComputed();
todoList.setServerData(['foo', 'bar']);
expect(todoList.todoItems[0].isSelected).toBe(false);
expect(todoList.todoItems[1].isSelected).toBe(false);
});
it('should select the correct item', function() {
const todoList = new TodoListComputed();
todoList.setServerData(['foo', 'bar']);
todoList.setSelected('bar');
expect(todoList.todoItems[0].isSelected).toBe(false);
expect(todoList.todoItems[1].isSelected).toBe(true);
});
});
For some reasons, I have to refactor my app and I use reactions instead of computed (for example because I have to display 10,000 items and the selection changed rapidly and calling TodoListComputed.setSelected
caused all computation to re-run on all items).
Therefore I change it to use reaction
(it seems that when I use autorun
I can use the same tests as for the computed
version):
import { autorun, computed, observable, action } from 'mobx';
export class TodoItemReaction {
readonly id: string;
@observable
isSelected = false;
constructor(id: string) {
this.id = id;
}
@action
setSelected(isSelected: boolean) {
this.isSelected = isSelected;
}
}
export class TodoListReaction {
@observable.shallow
private serverData: string[] = [];
@observable
selectedId = '';
constructor() {
reaction(
() => [this.selectedId, this.todoItems],
() => {
this.todoItems.forEach(t => {
t.setSelected(t.id === this.selectedId);
});
}
);
}
@action
setServerData(data: string[]) {
this.serverData = data;
}
@action
setSelected(id: string) {
this.selectedId = id;
}
@computed
get todoItems() {
return this.serverData.map(d => new TodoItemReaction(d));
}
}
Question: How to test the autorun
version?
Bonus Question: How to test the code that it does not matter if I use autorun
or computed
?
You should be able to test both implementations regardless of how you implement. BTW, I think that this should be true to any test, avoid "knowing" the implementation, test the requirements.
The issue with your implementation is the usage of @computed
on the todoItems
list, when you set the isSelected
of the item on the reaction using the setSelected
. This will cause the @computed
todoItems
to be recalculated.
This is due to the the fact that you set the isSelected
to false
when you initialize the class.
In other words, the @computed
todoItems
is "listening" to changes in each item's @observable
isSelected
.
Here's a very simple and fast implementation (Without reaction
):
export class TodoItem {
readonly id: string;
@observable isSelected = false;
constructor(id: string) {
this.id = id;
}
@action
setSelected(isSelected: boolean) {
this.isSelected = isSelected;
}
}
export class TodoList {
@observable.shallow
todoItems: TodoItem[] = [];
@action
setServerData(data: string[]) {
this.todoItems = data.map((d) => new TodoItem(d));
}
@action
setSelected(id: string) {
this.todoItems.forEach((t) => t.setSelected(t.id === id));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With