Software Development, english, web engineering

Harry Potter and the JavaScript Fatigue – Part 3

This is the last part (for now) of a series of articles on Vue.JS frontend development. In case you missed them, here is Part 1: "The Philosopher's UI" and Part 2:"The Chamber of Storage".

You can pull the App from here! If you do so, run git checkout part3 for this part, the final App is git checkout final.

Part 3: The Prisoner of Api

You can create and change Todos but one major draw-back is that you lose all state after restarting. You feel that having to apply a reappeariamos spell is a huge time-waster. So you ponder different solutions. Your idea is to banish the Todos to a place like Azkaban. To imprison them frozen in time in a basement of data, a database ment. You find such basements in the internet and after some consideration you decide to give either Cassandra or MongoDB a try. But you would never use Voldemort. Never.

hogwarts-blau


The deus-ex-machina-backend

Because a wizard does not submit himself to backend work, you ask your house elf to write some backend for you. And he quickly comes up with a Reactive Repository:

public interface TaskRepository extends ReactiveMongoRepository<Task, uuid=""> {
}

The little creature defines an Id on each task:

public class Task {
    @Id
    public final UUID id;
    public final String title;

    public Task(final String title) {
        this.id = UUID.randomUUID();
        this.title = title;
    }
}

The elf tells you something about Command Response Segregation and how Hexagonal Architectures is the greatest discovery since coffee mugs.

public class AddTask {
    @NotBlank(message = "Task title must not be blank!")
    public final String title;

    public AddTask(@JsonProperty("title") final String title) {
        this.title = title;
    }
}

He talks and talks. Now he mentions reactive backends and how reality is nothing more than a stream of past events in a message queue. You start to pity him and his simple elf mind:

@Component
@Validated
public class TasksService {

    @Autowired
    private TaskRepository repository;


    public Flux<Task> list() {
        return repository.findAll();
    }

    public Mono<Task> addTask(@NotNull AddTask command) {
        return repository.insert(new Task(command.title));
    }
}

He finally starts to make sense when he talks about the Rest Api. He will define a GET and a POST endpoint to fetch all tasks or post a new one.

@RestController
public class TaskApi {
    private static final String BASE_REST_PATH = "/api";
    private static final String API_PATH = "/tasks";
    private static final String APPLICATION_JSON = "application/json; charset=UTF-8";

    @Autowired
    TasksService service;

    @CrossOrigin(origins = "http://localhost:8080")
    @RequestMapping(
            value = BASE_REST_PATH + API_PATH,
            method = RequestMethod.GET,
            produces = APPLICATION_JSON
    )
    public Flux list() {
        return this.service.list();
    }

    @CrossOrigin(origins = "http://localhost:8080")
    @RequestMapping(
            value = BASE_REST_PATH + API_PATH + "/add",
            method = RequestMethod.POST,
            consumes = APPLICATION_JSON,
            produces = APPLICATION_JSON
    )
    public Mono add(@RequestBody @Validated final AddTask body) {
        return this.service.addTask(body);
    }
}


Asynchrounus testing

So the only thing left to do is writing an Api that calls the backend and updates the state of the store. That should not be too hard to do. First you have to install a library to make calling the backend a bit easier.

You let yarn do the magic: yarn install axios. And now you describe the api. You want it to fetch tasks und unwrap them from the more technical Axios Response. Also if an error occurs you must hand it down to the caller.

import * as TasksApi from "../../src/api/tasks";
import axios from 'axios';

describe("tasks api", () => {
  beforeEach(() => {
    axios.get = jest.fn();
  });

  it("maps fetched tasks into response", () => {
    const tasks = [];
    axios.get.mockReturnValueOnce(Promise.resolve({data: tasks}));
    return expect(TasksApi.fetchTasks()).resolves.toEqual(tasks);
  });

  it("hands errors down", () => {
    const error = {};
    axios.get.mockReturnValueOnce(Promise.reject(error));
    return expect(TasksApi.fetchTasks()).rejects.toEqual(error);
  });
});

These two tests also use Jest’s async testing tools and mock away the axios.get method using Jest! (Note they are not annotated as async, but return a promise!)

The api itself is pretty easy to build now.

import axios, {AxiosPromise} from 'axios';
import {Task} from "../domain/Task";

const config = {
  baseURL: 'http://localhost:4242'
};

function unwrapAxiosResponse(request: AxiosPromise): Promise {
  return new Promise((resolve, reject) => {
    request
      .then(response => resolve(response.data))
      .catch(error => {
        reject(error)
      })
  });
}

export function fetchTasks(): Promise<Array<Task>> {
  return unwrapAxiosResponse(axios.get<Array<Task>>('/api/tasks', config));
}

 

Now that the Api is ready, it makes sense to write tests for the Actions. Actions do not change the state, they only invoke mutations, as you saw in the previous Part. So this is what you define here.

  1. If the Api succeeds, it calls a FetchTasksSucceeded Mutation
  2. If the Api fails, it calls a FetchTasksFailed Mutation

You need to mock the fetchTasks method of the Api and have it return Promises. After looking at the Vuex documentation, you find out that Actions can return Promises. So you can even await the action from your testing code!

describe("fetch tasks", () => {
  beforeEach(() => {
    TasksApi.fetchTasks = jest.fn();
  });

  it("calls success if api succeeds", async () => {
    const context = mockContext(undefined);
    const tasks = [];
    TasksApi.fetchTasks.mockReturnValueOnce(
      Promise.resolve(tasks)
    );

    await actions.fetchTasks(context, {
      type: Actions.FetchTasks
    });
        
    expect(context.commit).toHaveBeenCalledWith({
      type: Mutations.FetchTasksSucceeded,
      tasks
    });
  });

  it("calls error handling on error", async () => {
    const context = mockContext(undefined);
    const error = {info: "Server Out of Potatoes Exception"};
    TasksApi.fetchTasks.mockReturnValueOnce(
      Promise.reject(error)
    );
     
    await actions.fetchTasks(context, {
      type: Actions.FetchTasks
    });
        
    expect(context.commit).toHaveBeenCalledWith({
      type: Mutations.FetchTasksFailed,
      error
    });
  });
});


Compared to the version that returns a promise this is more succinct. You can await any promise and then assert things.

Implementing the Action is again pretty much straight-forward.

[Actions.FetchTasks] ({commit}, payload: FetchTasks): Promise<void> {
  return TasksApi.fetchTasks()
    .then(tasks => {
      commit({
        type: Mutations.FetchTasksSucceeded,
        tasks
      });
    })
    .catch(error => {
      commit({
        type: Mutations.FetchTasksFailed,
        error
      });
    });
},


This was  –  to your surprise  –  less painful than the Herbology courses at Hogwarts. But only a little bit. 

Posting data

Fetching an empty list from a database ment is not of much use. You have to fill it somehow. So you will use the POST endpoint the elf defined and write your api function.

beforeEach(() => {
  axios.get = jest.fn();
  axios.post = jest.fn();
});
describe("add task", () => {
  it("maps added task into reponse", () => {
    const task = new Task("task");
    axios.post.mockReturnValueOnce(Promise.resolve({data: task}));
    return expect(TasksApi.addTask("task")).resolves.toEqual(task);
  });

  it("hands error down", () => {
    const error = {};
    axios.post.mockReturnValueOnce(Promise.reject(error));
    return expect(TasksApi.addTask("task")).rejects.toEqual(error);
  });
});

export function addTask(title: string): Promise<Task> {
  return unwrapAxiosResponse(axios.post<Task>('/api/tasks', {title}, config));
}


This is exactly the same as for your fetching endpoint. But you did not specify that the way data gets to axios. Adding an extra expect statement might make sense:

expect(axios.post.mock.calls[0][1]).toEqual({title});

Here you could also specify that the correct request headers are set. The downside about these tests is that they depend on the library used. But this goes for all tests against the backend. In a perfect world, we would create a mocked endpoint using something like Wiremock. Unfortunately, I do not know a reliable method to use such tests in the frontend (yet).

You enhance the AddTask action by calling the api.

beforeEach(() => {
  TasksApi.addTask = jest.fn();
});

it("adds tasks if they cannot be found", async () => {
  const context = mockContext(undefined);
  const taskTitle = "title";
  const task = new Task(taskTitle);
  TasksApi.addTask.mockReturnValueOnce(
    Promise.resolve(task)
  );

  await actions.addTask(context, {
    type: Actions.AddTask,
    title: taskTitle
  });

  expect(context.commit).toHaveBeenCalledWith({
    type:  Mutations.AddTaskSucceeded,
    task
  });
});

return TasksApi.addTask(title)
  .then(task => {
    commit({
      type: Mutations.AddTaskSucceeded,
      task
    });
  })
  .catch(error => {
    commit({
      type: Mutations.AddTaskFailed
    });
  });

The action and your test became asynchronous but not much changed besides that. The Action itself is nothing more than an api call.

You do not have to create a Task object anymore, because you get one from the Api and it already contains an ID.

A type casting Api

Wait it contains what?

The task object you receive from the Backend contains an Id field. (You vaguely remember the house elf mentioned that). But your task model in the frontend does not have such a thing. You realise that all the type-safety in your backend calling expressions like

axios.get<Array<Task>>('/api/tasks', config)

is nothing more than a compile-time illusion. You do not like illusions. True wizards do not deal in illusions, only in like actual real magic. Talking to a backend that might give you any object it wants to. Reality is outrageous! You have to do something about it (And now you can appreciate that you decided to put the api in its own layer)!

What you want is something like this:

it("maps fetched tasks into response", () => {
  const rawTask = {id: "some id", moreData: "garble garble", title: "title"};
  const tasks = [rawTask];
  axios.get.mockReturnValueOnce(Promise.resolve({data: tasks}));

  const promise = TasksApi.fetchTasks();
  promise.then(value => {
    expect(value).toHaveLength(1);
    expect(value[0]).toBeInstanceOf(Task);
    expect(value[0]).toEqual(new Task(rawTask.id, rawTask.title))
  });
  return promise;
});

You remove all the unknown fields from the result and each object must have the right types in the api. That is the wizard’s way.

First you add the id to the Task

export class Task {
  id: string;
  title: string;

  constructor(id: string, title: string) {
    this.id = id;
    this.title = title;
  }
}

and then you put another function into the api that casts your objects onto the right type.

function unwrapAxiosResponse<T>(request: AxiosPromise<object>, responseMapper: (object) => T): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    request
      .then(response => {
        resolve(responseMapper(response.data))
      })
      .catch(error => {
        reject(error)
      })
  });
}

export function fetchTasks(): Promise<Array<Task>> {
  return unwrapAxiosResponse(axios.get('/api/tasks', config), (object) => {
    return object.map(task => new Task(task.id, task.title))
  });
}


You do not pretend anymore that you already get the correct type from the backend, you map to your own type.

Casting Refactoriamos

All these Promises with their catch-then-resolve-reject semantic are very verbose and hard to read. You learned the Refactoriamos spell at Hogwarts and thought you could try it out on your api code.

import axios from 'axios';
import _ from 'lodash';
import {Task} from "../domain/Task";

const config = {
  baseURL: 'http://localhost:4242'
};

export async function fetchTasks(): Promise<Array<Tasks>> {
  const { data } = await axios.get('/api/tasks', config);
  return _.map(data, (task) => new Task(task.id, task.title));
}

export async function addTask(title: string): Promise<Task> {
  const { data } = await axios.post('/api/tasks', {title}, config);
  return new Task(data.id, data.title);
}

This is all that remained after the spell evaporated many of your lines. And all tests are still running green! What a great spell indeed. By making all functions asynchronous, it removed the helper function completely. Your code is no longer in the complicated swamp of Promise handling.

You cast the spell again, this time on your AddTask action and you get:

async [Actions.AddTask] ({getters, commit}, {title}: AddTask) {
   try {
     const foundTask = getters.taskByTitle(title);
     if(_.isNil(foundTask)) {
       const task = await TasksApi.addTask(title);
       commit({ type: Mutations.AddTaskSucceeded, task });
     }
   } catch(error) {
     commit({type: Mutations.AddTaskFailed, title });
   }
 },


On first glance, the spell did not do much. But you can see that it got rid of the chain of then s and the catch function somewhere. You have a nice try catch block instead. You apply the spell to your other actions in the same way. And feel more confident with your codebase.

You also cast the spell on some of your tests, they stop being so hideous.

it("maps fetched tasks into response", async () => {
  const rawTask = {id: "some id", moreData: "garble garble", title: "title"};
  const tasks = [rawTask];
  axios.get.mockReturnValueOnce(Promise.resolve({data: tasks}));

  const tasks = await TasksApi.fetchTasks();
  expect(tasks).toHaveLength(1);
  expect(tasks[0]).toBeInstanceOf(Task);
  expect(tasks[0]).toEqual(new Task(rawTask.id, rawTask.title));
});

Again instead of creating Promises you just await a result and use it.

There is also vuex-saga based on redux-saga that takes these ideas even further. But, by the time of writing vuex-saga has a bug that seems to make it harder to handle errors. Also sagas might not be necessary for every team.

That’s it for now. You feel relieved. Finally, you can think about the important topics of web development. Like what color you want for important todos or how many pixels of border radius are necessary for your Textboxes to impress Ginny Weasley.
    
About Maximilian Schuler

I am working at itemis as a software engineer at the web engineering team in Hamburg. As a full-stack developer I am at home in all domains of web development: from client-side development over data-intensive backend applications to platform engineering.