Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test mobx reaction?

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?

like image 901
Michael_Scharf Avatar asked Dec 08 '18 01:12

Michael_Scharf


1 Answers

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

like image 145
gilamran Avatar answered Nov 04 '22 13:11

gilamran