React dan TDD dalam 10 Menit — Part 1

Muhammad Rizki Rijal
Wonderlabs
Published in
12 min readApr 6, 2017

Mungkin banyak diantara developer yang ketika mendengar istilah TDD atau test-driven development akan merasa overwhelmed, tapi sebenarnya TDD tidaklah menakutkan. Artikel ini dibuat untuk teman-teman yang ingin mengenal atau memulai TDD practice pada front-end development, khususnya React.

Sedikit selingan, di wonderlabs, kami secara intensif menggunakan nodejs dan react untuk membangun aplikasi web dengan berbagai scope dan skala, tapi tidak terbatas pada itu saja. Code quality adalah salah satu yang utama, oleh karena itu, testing juga menjadi sangat penting dalam mengembangkan sebuah aplikasi di wonderlabs.

Manfaat Test

Sebelum kita mulai, kenapa kita harus membuat test untuk aplikasi yang kita buat? Diantara manfaat membuat test adalah:

  • Ketika aplikasi kita mempunyai coverage yang baik (mayoritas codebase tercover oleh test), Kita akan merasa percaya diri jika harus mengubah suatu bagian pada aplikasi kita. Saat kita mengubah bagian tersebut, dan ada bagian yang lain menjadi broken kita akan segera mengetahuinya.
  • Mengurangi bug pada aplikasi. Walaupun testing tidak menjamin aplikasi kita bebas bug, tetapi kita bisa mencegah beberapa hal yang berpotensi menjadi bug.
  • Kita menjadi terbiasa mendesain sebelum mengerjakan. Beberapa studi telah dilakukan dan hasilnya TDD sangat efektif meningkatkan produktifitas, karena ada objektif yang harus kita capai, yaitu menjadikan semua test case passed.
  • Dan lain-lain.

Saran buat teman-teman, ketika membuat komponen wajib membuat test nya juga. Memang di awal-awal terasa berat dan mungkin kepikiran gak worthed. Tapi percayalah, ketika aplikasi semakin besar, test-test yang kita buat akan menjadi penolong kita.

Todo App — Overview

Kenapa aplikasi todo? Selain mudah, kita juga punya waktu lebih banyak untuk fokus kepada tujuan awal artikel ini dibuat, yaitu untuk pengenalan TTD practice.

Okay, sesuai judul artikel ini, scope test yang akan kita buat adalah unit test, yaitu kita akan membuat test terisolasi hanya untuk satu komponen. Gampangnya, jika suatu komponen mempunyai komponen child, maka kita tidak akan peduli dengan behavior komponen child tersebut, cukup fokus pada komponen yang ingin kita test.

Fitur aplikasi todo yang akan kita buat seperti ini:

  • Bisa menampilkan semua todo.
  • Bisa filter todo yang telah selesai.
  • Bisa menambahkan todo.
  • Todo yang telah selesai akan tercoret.

Jest dan Enzyme

Kita akan menggunakan testing framework Jest bawaan create-react-app serta Enzyme sebagai testing utility. Keduanya merupakan standar de-facto saat ini di komunitas react.

Agar bisa menerapkan TDD, tentunya kita harus tahu terlebih dahulu tentang Jest dan Enzyme.

Jest

Jest adalah testing framework dengan jargon “Painless Javascript Testing”. Yang saya senangi dari testing framework besutan Facebook ini adalah running test suits-nya yang cepat. By default, test suits akan dijalankan secara paralel oleh Jest, dan ini sangat cocok untuk membuat unit test untuk komponen-komponen React yang akan kita buat.

Jest digunakan untuk menjalankan test suits, serta untuk membuat assertion. Contoh assertion:

expect(2 + 4).toBe(6);
expect('hello'.toUpperCase()).toBe('HELLO');

Enzyme

Enzyme adalah testing utility untuk React. Penggunaan Enzyme bisa dibilang mudah, sebab api-nya mirip dengan jQuery. Dengan utility ini kita bisa memanipulasi komponen, traversing komponen-komponen react, simulasi event dan lainnya.

Api yang akan dipakai dari Jest dan Enzyme, akan kita bahas dibagian selanjutnya.

Bagaimana caranya?

Untuk setiap komponen, kita harus menspesifikasi ability / kemampuan / behaviour dari komponen tersebut. Kemudian kita buat file test-nya dan menuliskan assertion berdasarkan behaviour yang kita spesifikasi. Setelah itu, kita implementasi hingga test-nya pass / berhasil dilewati. Ketika test berhasil dilewati, kita bisa optimize kode yang sudah ada dengan code refactoring.

Pertama, install create-react-app

$ yarn add -g create-react-appatau$ npm install -g create-react-app

Kemudian scaffold project React, beri nama todo-app

$ create-react-app todo-app

Ketika selesai masuk ke direktori project.

$ cd todo-app

Install enzyme dan react-addons-test-utils

$ yarn add --dev enzyme react-addons-test-utilsatau $ npm i -D enzyme react-addons-test-utils

Jika sudah, mari kita buat file test pertama.

Komponen 1: TodoList

Spesifikasi

  • Menampilkan judul komponen yang benar (easy peasy lol)
  • Menampilkan semua todo
  • Filter todo yang sudah selesai / done

Buat file TodoList.spec.js di folder src/components/

// src/components/TodoList.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import TodoList from './TodoList';
// 1. Menampilkan header / judul yang benar. yaitu: My Todo List
it('shows correct header');
// 2. Bisa filter todo berdasarkan kategori: all (semua todo) dan done (todo yang sudah selesai)
it('should able to filter todos');

it(name, fn) atau test(name, fn) merupakan global api dari Jest. Gunanya untuk mendefinisikan test. Jika parameter kedua dikosongkan, maka test dianggap skipped/pending. Argument kedua berupa function yang berisi assertions.

Lalu jalankan

$ npm test -- --verbose

Output-nya kurang lebih akan seperti ini:

Sekarang kita buat file TodoList.js di folder yang sama. Setelah file ini di-save, Jalankan kembali perintah sebelumnya. Outputnya akan seperti ini:

Okay, kita siap membuat desain komponen.

// src/components/TodoList.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import TodoList from './TodoList';
// 1. Menampilkan header / judul yang benar. yaitu: My Todo List
it('shows correct header', () => {
const subject = shallow(<TodoList entries={[]} />);
expect(subject.find('.todo-list__header').text()).toBe('My Todo List');

});
// 2. Bisa filter todo berdasarkan kategori: all (semua todo) dan done (todo yang sudah selesai)
it('should able to filter todos');

Penjelasan

const subject = shallow(<TodoList entries={[]} />);

shallow() hanya akan me-render komponen yang ditarget, yaitu TodoList. Jika komponen tersebut mempunyai komponen child, maka komponen child tidak akan di-render.

shallow akan me-return ShallowWrapper.

Beberapa api dari ShallowWrapper:

  • find(EnzymeSelector) => ShallowWrapper — untuk mencari suatu elemen
  • text() => String — untuk mendapatkan representasi teks dari elemen
  • at(index) => ShallowWrapper — untuk mendapatkan elemen berdasarkan index dari wrapper yg sekarang
  • state(key) =>Any — untuk mendapatkan state berdasarkan key
expect(subject.find('.todo-list__header').text()).toBe('My Todo List');

subject.find(‘.todo-list__header’).text() untuk mengambil value text dari elemen dengan class .todo-list__header. Value tersebut kita pass sebagai argument dari fungsi expect().

Cara bacanya:

subject (TodoList) tolong cari element dengan class .todo-list__header menggunakan method find, jika dapat ambil value text dari element tersebut menggunakan method text().

toBe() untuk mencocokan value yang kita dapatkan tadi, dengan value yang kita harapkan.

Setelah di-save, error yang muncul akan seperti ini:

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: object. You likely forgot to export your component from the file it's defined in.

Error ini muncul karena kita belum implementasi. Sekarang kita buat komponen di file TodoList.js.

import React, { Component } from 'react';class TodoList extends Component {
state = {};
render() {
return (
<div className="todo-list">
<h3 className="todo-list__header">My Todo list</h3>
</div>
);
}
}
export default TodoList;

Save dan…error!

Kenapa ya 😅…

Tapi kalau dilihat-lihat, udah benar…

Eh, wait…

My Todo List” !== “My Todo list”.

Huruf l kita ganti jadi L.

Dan…

🎉🎉🎉 Selamat! 🎉🎉🎉. Kita berhasil memulai Test-Driven Development.

Yep, ke test selanjutnya. Disini kita punya kesempatan untuk mendesain struktur data yang untuk sebuah props komponen ini. Kira-kira seperti ini:

const todoEntries = [
{ id: 1, text: 'Hello', done: false },
{ id: 2, text: 'There', done: true },
{ id: 3, text: "It's cool, isn't it?", done: false },
];

Atribut done pada setiap element dari todoEntries menandakan apakah todo tersebut sudah selesai atau belum. Jika true, maka todo telah selesai, jika false maka todo belum selesai.

Variable todoEntries akan kita pass melalui props entries di komponen TodoList seperti ini:

<TodoList entries={todoEntries} />

Kemudian kita akan menampilkan todo dalam bentuk list dari data di props ini . Sekarang, mari kita buat test nya.

Lalu kita implementasi:

import React, { Component } from 'react';class TodoList extends Component {
state = {
filter: 'all',
};
render() {
return (
<div className="todo-list">
<h3 className="todo-list__header">My Todo List</h3>
<select className="todo-list__filter">
<option value="all">All</option>
<option value="done">Done</option>
</select>

{this.props.entries.map(todo => (
<div key={todo.id} className="todo-list__todo">
{todo.text}
</div>
))}

</div>
);
}
}
export default TodoList;

Kemudian error…

Kode pada line TodoList.spec.js:47:51 adalah:

47 | expect(subject.find('.todo-list__todo').length).toBe(1);

Yang mana sebelumnya kita mencoba mengganti value dari .todo-list__filter dari ‘all’ menjadi ‘done’. Yang perlu kita lakukan adalah ketika value tersebut berubah, kita harus mengubah state filter menjadi ‘done’.

import React, { Component } from 'react';class TodoList extends Component {
state = {
filter: 'all',
};
onFilterChange = (event) => {
this.setState({
filter: event.target.value,
});
};
render() {
return (
<div className="todo-list">
<h3 className="todo-list__header">My Todo List</h3>
<select onChange={this.onFilterChange} className="todo-list__filter">
<option value="all">All</option>
<option value="done">Done</option>
</select>
{this.props.entries.map(todo => (
<div key={todo.id} className="todo-list__todo">
{todo.text}
</div>
))}
</div>
);
}
}
export default TodoList;

Dan… masih error lagi … 😅

Setiap kali value dari .todo-list__filter berubah, maka state filter juga akan ikut berubah. Dengan implementasi ini saja masih belum cukup. Karena walaupun kita merubah state filter, rendering tetap menggunakan data yang sama, dalam hal ini 3 todo.

Yang perlu kita lakukan selanjutnya, tinggal bagaimana cara kita menampilkan todos berdasarkan filter yang sudah kita simpan di state.

import React, { Component } from 'react';class TodoList extends Component {
state = {
filter: 'all',
};
onFilterChange = (event) => {
this.setState({
filter: event.target.value,
});
};
render() {
const { filter } = this.state;
let { entries } = this.props;
// Jika filter nya adalah done
// maka ambil todo yang punya property done === true
// (lihat props yang di pass, hanya 1 element dengan property done === true)
// Jika filter nya adalah all
// maka block if dibawah tidak akan dieksekusi sehingga
// akan value entries tetap sama, yaitu 3 item
if (filter === 'done') {
entries = entries.filter(todo => todo.done);
}
return (
<div className="todo-list">
<h3 className="todo-list__header">My Todo List</h3>
<select onChange={this.onFilterChange} className="todo-list__filter">
<option value="all">All</option>
<option value="done">Done</option>
</select>
{entries.map(todo => (
<div key={todo.id} className="todo-list__todo">
{todo.text}
</div>
))
}
</div>
);
}
}
export default TodoList;

Dan…tada!!!

Semua test pass! Sampai disini, kita telah berhasil menulis test (desain) terlebih dahulu, kemudian implementasinya. Keren 😎

Sekarang, komponen TodoList kita seperti ini

<div className="todo-list">
<h3 className="todo-list__header">My Todo List</h3>
<select onChange={this.onFilterChange} className="todo-list__filter">
<option value="all">All</option>
<option value="done">Done</option>
</select>
{entries.map(todo => (
<div key={todo.id} className="todo-list__todo">
{todo.text}
</div>

))}
</div>

Bagaimana jika bagian ini kita ekstrak menjadi komponen sendiri?

<div key={todo.id} className="todo-list__todo">
{todo.text}
</div>

Komponen ini kita namakan Todo.js

Komponen 2: Todo

Spesifikasi

Komponen ini spesifikasinya hanya:

  • Menampilkan text todo
  • Jika todo sudah selesai, tampilkan garis line-through dan jika tidak garisnya tidak tampil.

Sepertinya komponen ini butuh dua props, text dan isDone. text untuk menyimpan teks dari todo, isDone untuk menandakan apakah todo ini sudah selesai atau belum. Ayo kita buat file testnya

// src/components/Todo.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import Todo from './Todo';
it('shows todo text');
it('shows line-through when it is done');
it('should not show line-through when it is not done');

Kemudian buat file Todo.js di folder yang sama

import React from 'react';const Todo = ({ text, isDone }) => (
<div className="todo">
Todo
</div>
);
export default Todo;

Output test kurang lebih akan seperti ini:

Sekarang kita mulai dari test yang pertama.

// src/components/Todo.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import Todo from './Todo';
it('shows todo text', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
}
);
it('shows line-through when it is done');
it('should not show line-through when it is not done');

Output:

Sekarang kita implementasi

import React from 'react';const Todo = ({ text, isDone }) => (
<div className="todo">
{text}
</div>
);
export default Todo;

Cool! ke test selanjutnya…

// src/components/Todo.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import Todo from './Todo';
it('shows todo text', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
});
it('shows line-through when it is done', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
expect(subject.prop('style').textDecoration).toBe('line-through');
}
);
it('should not show line-through when it is not done');

Implementasi:

import React from 'react';const Todo = ({ text, isDone }) => (
<div className="todo" style={{ textDecoration: isDone ? 'line-through' : 'none' }}>
{text}
</div>
);
export default Todo;

Output:

Okay. Next…

// src/components/Todo.spec.jsimport React from 'react';
import { shallow } from 'enzyme';
import Todo from './Todo';
it('shows todo text', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
});
it('shows line-through when it is done', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
expect(subject.prop('style').textDecoration).toBe('line-through');
});
it('should not show line-through when it is not done', () => {
const subject = shallow(<Todo text="Attend React Conf 17" isDone={false} />);
expect(subject.find('.todo').text()).toBe('Attend React Conf 17');
expect(subject.prop('style').textDecoration).toBe('none');
}
);

Oya, ingat, setelah save lihat output dari test. Implementasi:

Sudah terwakili di implementasi sebelumnya. 

Output:

Sebenarnya test kedua dan ketiga bisa digabung 😂

Sebelum Bosan…

Well, kita sudah membuat 2 komponen dengan approach TDD. Sekarang mari kita lihat hasilnya.

Pertama gunakan komponen Todo di dalam TodoList

import React, { Component } from 'react';
import Todo from './Todo';
class TodoList extends Component {
state = {
filter: 'all',
};
onFilterChange = (event) => {
this.setState({
filter: event.target.value,
});
};
render() {
const { filter } = this.state;
let { entries } = this.props;
if (filter === 'done') {
entries = entries.filter(todo => todo.done);
}
return (
<div className="todo-list">
<h3 className="todo-list__header">My Todo List</h3>
<select onChange={this.onFilterChange} className="todo-list__filter">
<option value="all">All</option>
<option value="done">Done</option>
</select>
{entries.map(todo => (
<Todo key={todo.id} text={todo.text} isDone={todo.done} />
))}

</div>
);
}
}
export default TodoList;

Namun ketika test dijalankan…

Why???

Oh wait, kalau dilihat di testnya, selector yang dimaksud adalah .todo-list__todo sedangkan selector di komponen Todo adalah .todo.

Okay… mari kita update test nya.

Udah males copas wkwk. Coba ganti .todo-list__todo menjadi .todo

Dan hasilnya …

What!!!??? Masih sama!!! 😡

Let me explain a lil bit…

Tadi kita menggunakan shallow untuk me-render komponen TodoList. Nah karena di dalam TodoList ada komponen child Todo, ternyata shallow tidak dapat menemukan elemen dengan class .todo (karena komponen Todo tidak di-render). Jadinya elemen yang ditemukan adalah nol!

Solusinya, kita ganti argument find() dari css selector (.todo) menjadi displayName selector.

Ketika komponen TodoList di-render menggunakan shallow, kita bisa menemukan komponen child menggunakan method find(), dengan argument string displayName (namun Todo tetap tidak di-render), seperti ini.

shallowWrapper.find('myComponentDisplayName');

Tambahkan displayName pada komponen Todo.

import React from 'react';const Todo = ({ text, isDone }) => (
<div className="todo" style={{ textDecoration: isDone ? 'line-through' : 'none' }}>
{text}
</div>
);
Todo.displayName = 'Todo';export default Todo;

kemudian ganti line ini

expect(subject.find('.todo').length).toBe(3);expect(subject.find('.todo').length).toBe(1);expect(subject.find('.todo').at(2).text()).toBe("It's cool, isn't it?");

menjadi

expect(subject.find('Todo').length).toBe(3);expect(subject.find('Todo').length).toBe(1);expect(subject.find('Todo').at(2).shallow().text()).toBe("It's cool, isn't it?");

shallow() kita panggil untuk me-render komponen Todo.

Dan akhirnya

In Action

Dari tadi tampilannya cuma command-line mulu. Sekarang ayo Kita tampilkan di browser.

// src/App.jsimport React, { Component } from 'react';
import TodoList from './components/TodoList';
import './App.css';
class App extends Component {
state = {
todos: [
{ id: 1, text: 'Hello', done: false },
{ id: 2, text: 'There', done: true },
{ id: 3, text: "It's cool, isn't it?", done: false },
],
};
render() {
return (
<div className="App">
{/* Pass state.todos ke TodoList */}
<TodoList entries={this.state.todos} />
</div>
);
}
}
export default App;

Penampakannya seperti ini:

todo app in action

Penutup

Kita sudah berhasil membuat komponen TodoList dan Todo dengan TDD. Di artikel selanjutnya kita akan membuat komponen TodoForm yang berguna untuk menambahkan todo ke state komponen App.

Notes

Software Versions

  • create-react-app v1.3.0
  • enzyme v2.7.1
  • react-addons-test-utils v15.4.2

--

--