PPL 2020 — Document Builder: Testing Model dan Migration Script Sequelize

Firman Hadi
pepeel
Published in
5 min readMar 8, 2020

Sequelize merupakan sebuah library ORM yang cukup terkenal di dunia Node.js. Dalam Document Builder, kami menggunakan library ini agar memudahkan kami dalam melakukan manipulasi database. Hanya dengan mengatur konfigurasi koneksi database, membuat definisi model dan menuliskan script untuk membuat tabel yang mewakili model tersebut, maka kita sudah bisa menggunakan model yang telah kita buat tanpa menyentuh bahasa SQL sama sekali. Berikut adalah contoh dari penggunaan Sequelize:

Contoh konfigurasi Sequelize
Contoh definisi model Category
Contoh script untuk membuat tabel Category (id, created_at, updated_at, deleted_at otomatis digunakan Sequelize)
Contoh penggunaan model Category

Begitulah kira-kira kemudahan menggunakan Sequelize. Apabila Anda tertarik, Anda bisa mempelajari lebih lanjut di halaman resminya. Pada tulisan kali ini, kami akan membahas bagaimana kami melakukan testing terhadap definisi model yang kami gunakan pada Document Builder.

Mengapa Test?

Anda mungkin akan berpikir: untuk apa sih testing model? Buang-buang waktu saja. Test ini sebenarnya sangat penting apabila Anda mengandalkan migration scripts-nya Sequelize karena Sequelize itu sendiri tidak meng-generate script secara otomatis seperti Django, maka ada potensi human error di script tersebut. Test yang akan kita buat akan menguji:

  • Konsistensi nama kolom tabel dengan atribut model yang dibaca oleh Sequelize, dan
  • Hasil migration script yang akan dijalankan sebelum diluncurkan ke staging/production.

Mengapa Tidak Mock?

Kami tidak menggunakan mock seperti Sequelize Mock. Menurut kami, apabila kami melakukan mock pada database, maka dua aspek di atas tidak akan teruji. Walaupun lebih cepat (dalam performa), melakukan mock pada database merupakan pekerjaan yang tidak mudah, dan migration script tidak akan bisa di-test juga.

Mempersiapkan Environment untuk Test

Karena model berinteraksi langsung dengan database, maka kita harus mempersiapkan database yang akan kita gunakan untuk test. Terdapat beberapa strategi:

  • Mempersiapkan database ke DBMS lokal yang digunakan untuk development: Hal ini bisa saja dilakukan, namun biasanya setelah test, maka kita harus membersihkan database secara manual, dan ini terkadang menyulitkan.
  • Mempersiapkan database kosong ke DBMS lokal khusus test: Mempersiapkan database bisa dilakukan secara manual. Apabila kita menggunakan automated pipeline (CI/CD) yang menjalankan test tersebut, maka mempersiapkannya akan lebih sulit.
  • Mempersiapkan in-memory database untuk setiap test: Cara ini yang kami lakukan. Untuk setiap test suite, maka akan dibuat in-memory database menggunakan SQLite3, dan migration script dijalankan terhadap database tersebut, dan setiap test suite selesai, maka database dikosongkan.

Untuk bagian berikutnya, akan dijelaskan langkah-langkah untuk strategi ketiga. Kami akan menggunakan framework testing Jest sebagai contoh, tetapi seharusnya framework lain tidak jauh beda cara kerjanya.

SQLite3 In-Memory Database Khusus Test

Mempersiapkan in-memory database SQLite3 sebenarnya cukup mudah. Kita akan mengatur Sequelize untuk menggunakan database tersebut dalam berkas konfigurasi koneksi database (biasanya config.js). Akan tetapi, pastikan bahwa Sequelize hanya menggunakan database tersebut hanya untuk test. Caranya adalah sebagai berikut:

  1. Apabila belum, Tambahkan package sqlite3.
  2. Manfaatkan process.env.NODE_ENV untuk membedakan environment test atau yang tidak. Secara default, Jest akan menggunakan NODE_ENV=test pada setiap test-nya.
  3. Gunakan konfigurasi berikut untuk environment test (username, password, logging bisa beda)
config = {
username: 'testdb',
passsword: 'pass',
dialect: 'sqlite',
storage: ':memory:',
logging: false
};

Sebagai contoh, berikut adalah konfigurasi yang kami gunakan pada DocumentBuilder:

const env = process.env.NODE_ENV || 'development';

let config;
if (env === 'test') {
// For unit tests, use in memory database provided by SQLite3.
config = {
username: 'testdb',
passsword: 'pass',
dialect: 'sqlite',
storage: ':memory:',
logging: false
};
} else {
config = {
// ...
};
}

module.exports = config;

Konfigurasi Migration untuk Test

Setelah itu, kita bisa membuat test terhadap model yang telah kita buat. Sebagai contoh, kode berikut melakukan test terhadap konsistensi kolom tabel dan keberadaan kolom deleted_at (yang digunakan oleh properti paranoid pada model Category di kode paling atas)

Contoh test pada model Category

Namun, apabila test tersebut dijalankan, maka Sequelize akan memberikan error bahwa tabel categories tidak ada:

SequelizeDatabaseError: SQLITE_ERROR: no such table: categories

Hal tersebut terjadi karena kita belum menjalankan migration script untuk membuat tabel categories tersebut. Maka, kita harus menjalankan migration script untuk setiap setup (beforeAll) dan melakukan undo migration untuk setiap teardown (afterAll). Bagaimana cara melakukannya?

Sayangnya, walaupun Sequelize mempunyai perintah untuk melakukan migration melalui sequelize-cli, ternyata melakukan migration secara manual melewati kode tidak mudah. Kita akan menggunakan library bernama Umzug untuk melakukan migration secara manual lewat kode. Umzug sendiri digunakan oleh Sequelize untuk melakukan migration-nya.

Setelah memasang library tersebut, maka kita bisa melakukan migration secara manual. Kita akan mengarahkan Umzug ke folder dimana migration script kita berada. Berikut adalah contoh utility class yang kami gunakan untuk melakukan migration dan membatalkan migration (serta method tambahan untuk menghapus semua isi dalam setiap tabel):

Utility class yang kami gunakan

Method migrateDatabase() akan melakukan migration dari script pertama sampai terakhir. Kebalikannya adalah method undoAllDatabaseMigrations() yang akan membatalkan migration dari akhir sampai awal (inklusif). Untuk menghapus semua isi dari setiap tabel, gunakan clearDatabaseData().

Catatan: Method clearDatabaseData() mungkin bisa saja gagal apabila Anda mempunyai tabel yang memiliki foreign key dengan constraint on delete: restrict karena proses penghapusan isi tabel tidak mengikuti urutan tertentu. Gunakan dengan hati-hati.

Setelah membuat masing-masing method untuk migration, maka kita tinggal menggunakannya di setiap test suite yang menguji model dan migration script kita. Strateginya yang akan digunakan adalah:

  • Sebelum sebuah test suite dimulai, lakukan migration.
  • Setelah sebuah test selesai, hapus semua isi setiap tabel.
  • Setelah sebuah test suite selesai, batalkan migration.

Dengan strategi tersebut, dijamin setiap test yang dijalankan akan berawal dari state tabel kosong (tidak akan terpengaruh oleh test lain), dan setiap test suite akan mulai dari state database kosong. Untuk Jest, dapat digunakan beforeAll, beforeEach, afterAll untuk menerapkan strategi tersebut.

import pick from 'lodash/pick';
import DbUtils from 'tests/server/models/utils/DbUtils';
import { Category } from 'server/models';

describe('Category model', () => {
beforeAll(async () => {
return DbUtils.migrateDatabase();
});

afterEach(async () => {
return DbUtils.clearDatabaseData();
});

afterAll(async () => {
return DbUtils.undoAllDatabaseMigrations();
});

it('has name, description, and isDeleted', async () => {
// ...
});

it('inserts multiple data properly', async () => {
// ...
});

it('uses paranoid table', async () => {
// ...
});
});

Sekarang, terlihat test-nya akan berhasil:

Test berhasil.

Untuk model yang lain, lakukan hal yang sama.

Kekurangan

Cara di atas tidaklah tanpa kekurangan. Sebenarnya, cara ini tidak begitu cocok untuk project dengan migration script yang sudah sangat banyak sehingga memperlambat test.

Alternatif lain yang bisa dilakukan adalah mempersiapkan satu database kosong yang sudah dijalankan migration script-nya, dan setiap test suite selesai, cukup hapus isi dari setiap tabel (seperti strategi yang kedua). Dengan hal tersebut, maka kita perlu mempersiapkan database di dalam proses CI/CD-nya.

Tim kami belum merasa perlu untuk melakukan strategi tersebut karena migration script yang kami gunakan masih sedikit.

Kesimpulan

Testing migration script dan definisi model Sequelize diperlukan untuk memastikan konsistensi antara atribut dan nama kolom pada database. Dalam melakukan testing tersebut, kita dapat menggunakan in-memory database. Hal ini lebih mudah karena kita tidak perlu memikirkan bagaimana cara melakukan mock database.

Sekian penjelasan saya dalam cara kami melakukan testing pada model dan migration script dari Sequelize. Jangan lupa untuk melihat artikel oleh anggota tim yang lain.

--

--