Mountebank ile API Dönüşlerinin Mocklanması
Merhabalar.
Bu yazımda geliştirmiş olduğumuz uygulamaların iletişime geçtiği API'ların dönüşlerinin (response) nasıl mocklanacağından bahsedeceğim.
Şöyle bir senaryoyu düşünelim. Bir Front-End uygulaması üzerinden (React, Vue veya Angular ile geliştirilmiş) bir API uygulamasına istek atıyoruz. Front-End uygulamamızın testlerini yazarken API uygulamasının da ayakta ve istediğimiz cevapları vermesini beklemekteyiz. Ancak Front-End uygulaması üzerinden atılan her istek API'ın bağlı bulunduğu veritabanında, cache sunucularındaki v.b. yerlerdeki verilerde değişiklik yapabilir. Bu değişiklikler sonucunda testlerin beklemiş oldukları verilerde de farklılıklar oluşmaya başlayacak ve doğru bir şekilde yazılmmış olan testler kırılmaya başlayacaktır.
Bu tip durumları önlemek adına Mountebank benzeri araçlardan yararlanabiliriz.
Mountebank, desteklemiş olduğu birden fazla protokol ile testlerimiz için gerekli olan istekleri ve cevapları dönmemizi sağlayan bir araçtır. HTTP, HTTPS, TCP, SMTP, LDAP ve GRPC gibi protokolleri mocklayabiliriz.
Mountebank aynı zamanda imposter mekanizması sayesinde mikroservis mimarisini de desteklemektedir.
Şimdi kısaca Mountebank'teki bazı kavramlara göz atalım.
Stubs
Stub'lar göndermiş olduğumuz istekleri ve cevapları kalıplar halinde tutan yapılardır. Örneğin /users/1
adresine gönderilen istek sonucunda hangi HTTP durum kodunun, hangi HTTP header'larının ve hangi HTTP içeriğinin gönderileceğini stub'lar sayesinde belirtmekteyiz.
Örnek stub dosyasının içeriği aşağıdaki gibidir:
const productStubs = [
{
predicates: [
{
equals: {
method: "GET",
path: "/products",
},
},
],
responses: [
{
is: {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
success: true,
data: [
{
id: "8F11AD71-D7E7-45EF-AF9E-D54EF0675C77",
name: "Macbook Pro",
price: 10,
},
],
}),
},
},
],
},
];
module.exports = productStubs;
Bu stub dosyasında /products
adresine GET
isteği atıldığı zaman JSON
formatında, HTTP 200
durum koduyla beraber bir JSON veri gönderilmektedir.
Imposters
Imposter'lar Mountebank'in üzerinde birden fazla uygulama veya servis çalışıyormuş gibi davranması sağlayan yapılardır. Örneğin bir web uygulaması için ayrı, mobil uygulama için ayrı imposter'lar tanımlayarak stub'ları bölmüş ve gruplamış olursunuz. Bu sayede stub dosyalarının içeriğinin karmaşıklaşmasının önüne geçmiş olursunuz. Aynı zamanda mikroservisleriniz mocklamak istediğiniz zaman imposter'lara başvurabilirsiz.
Örnek imposter dosyasının içeriği aşağıdaki gibidir:
const mbhelper = require("../mbhelper");
const productStubs = require("../stubs/product");
function addStubs() {
const stubs = [...productStubs];
const imposter = {
port: 4006,
protocol: "http",
stubs: stubs,
};
return mbhelper.postImposter(imposter);
}
module.exports = { addStubs };
Yukarıda HTTP
protokolü üzerinde ve 4006
portundan yayın yapan bir imposter tanımı yaptık.
Örnek Uygulama
Kavramlardan kısaca bahsettikten sonra örnek uygulamamıza geçebiliriz. Yapacağımız örnek uygulamada kullanıcılar ile ilgili işlemlerin gerçekleştirildiği bir stub tanımı yapıp bunu bir imposter üzerinden yayına açacağız.
Öncelikle aşağıdaki komutları yazarak Mountebank ve ilgili paketlerin kurulumunu gerçekleştirelim.
npm init -y
npm i mountebank node-fetch
Paketlerin kurulumları tamamlandıktan sonra aşağıdaki gibi bir klasör ve dosya yapısı oluşturalım.
Oluşturmuş olduğumuz klasör ve dosyalar şu işlere yaramaktadır:
- imposters: Bu klasörün altına ekleceğimiz her dosya bir imposter'ı tanımlamaktadır. Bu örnek uygulamada bir tane imposter bulunmaktadır.
- stubs: Bu klasörün altına ise stub tanımlarımının bulunduğu dosyalar bulunacaktır.
- index.js: Bu dosyada Mountebank'in server instance'ına ait ayarlamaları ve stub'ları kayıt eden bilgiler bulunmaktadır.
- mbhelper.js: Bu dosya tanımlamış olduğumuz imposter'ları Mountebank'e iletmek için yardımcı fonksiyonları içerir.
Öncelikle mbhelper.js
dosyasının içeriğini aşağıdaki gibi değiştirelim.
const fetch = require("node-fetch");
function postImposter(body) {
const url = "http://127.0.0.1:4001/imposters";
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
module.exports = { postImposter };
Bu dosya içerisinde imposter'ların Mountebank'e kayıt edilmesi için yardımcı bir fonksiyon yazdık. Mountebank bu senaryoda 4001 portundan yayın yapmaktadır. Bunun ayarını index.js dosyasında yapacağız.
Şimdi ise index.js
dosyamızın içeriğini aşağıdaki şekilde düzenleyelim.
const mb = require("mountebank");
const apiImposter = require("./imposters/api");
const mbServerInstance = mb.create({
port: 4001,
pidfile: "./mb.pid",
logfile: "./mg.log",
protofile: "./protofile.json",
ipWhitelist: ["*"],
});
mbServerInstance.then(function () {
apiImposter.addStubs();
});
Bu dosyada ise Mountebank instance'ının oluşturulması için gerekli ayarlamaları yaptık. 4001 portundan yayın yapacak ve tüm IP adreslerinden gelen isteklere cevap verecek şekilde ayarlamaları yaptık. Mountebank instance'ı çalışır hale geldikten sonra apiImposter.addStubs()
metodu ile imposter'ımızın içinde tanımlı olan tüm stub'ların Mountebank tarafından erişilebilir hale gelmesini sağladık.
Artık temel tanımlamaları yaptığımıza göre imposter ve stub tanımlamalarını yapmaya başlayabiliriz.
Öncelikle imposter tanımını yapalım. Bunun için imposters/api.js
dosyamızın içeriğini aşağıdaki şekilde düzenleyelim.
const mbhelper = require("../mbhelper");
const userStubs = require("../stubs/user");
function addStubs() {
const stubs = [...userStubs];
const imposter = {
port: 4006,
protocol: "http",
stubs: stubs,
};
return mbhelper.postImposter(imposter);
}
module.exports = { addStubs };
Bu dosyada yapılan işlemlerden kısaca bahsedelim. Imposter, HTTP protokolü ve 4006 portundan yayın yapacak ve stubs/user.js
dosyasının içerisindeki verileri kullanacak şekilde tanımlama yaptık. mbhelper.postImposter()
metodu ile de Mountebank'e imposter bilgileri ilettik.
Artık stub dosyamızı oluşturabiliriz. Bu işlem için stubs/user.js
dosyasını aşağıdaki gibi düzenleyelim.
const userStubs = [
{
predicates: [
{
equals: {
method: "GET",
path: "/users",
},
},
],
responses: [
{
is: {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
success: true,
data: [
{
id: "8F11AD71-D7E7-45EF-AF9E-D54EF0675C77",
name: "Mennan",
surname: "Köse",
},
{
id: "36DDB739-7541-466D-898E-848F65C9323D",
name: "Anıl",
surname: "Atalay",
},
{
id: "62133666-03BE-4A94-89B6-0FDFF5532053",
name: "Selçuk",
surname: "Ermaya",
},
{
id: "D34AED08-AC23-44B1-A58E-F72203CF8ED5",
name: "Dilara",
surname: "Tezel",
},
{
id: "50E95E5B-6D17-4010-9704-2E4CF80A8FC9",
name: "Ayşe Sena",
surname: "Paluluoğlu",
},
{
id: "18BA4B9C-11BA-41DC-8037-40EBCA9EECE7",
name: "İbrahim",
surname: "Şentürk",
},
{
id: "93198A19-1A4A-47D5-820A-41F3BECA42F6",
name: "Ahmet Ensar",
surname: "Köprülü",
},
{
id: "27D480BE-1882-4033-BCE4-D5B7B03CBB97",
name: "Mustafa",
surname: "Ermaya",
},
],
}),
},
},
],
},
{
predicates: [
{
equals: {
method: "POST",
path: "/users/8006A836-FB25-4801-BB71-57C9E6A05065",
},
},
],
responses: [
{
is: {
statusCode: 404,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
success: false,
data: null,
message: "User not found!",
}),
},
},
],
},
];
module.exports = userStubs;
Burada 2 adet endpoint tanımı gerçekleştirdik. İlki /users
adresine GET
isteği atıldığında, diğeri ise /users/8006A836-FB25-4801-BB71-57C9E6A05065
adresine POST
isteği atıldığı zaman dönülecek cevapları içermektektedir. predicates bölümünde yapılan isteğin tanımını içermektedir. responses bölümünde ise dönülecek olan cevaba ait olan bilgiler bulunmaktadır. HTTP durum kodu, HTTP header'ları, body gibi bilgiler burada tanımlanmaktadır.
Şimdi yaptığımız işlemleri test edelim. Uygulamayı hızlıca ayağa kaldırmak için package.json
dosyasına mock
isminde bir script tanımı yapacağız. package.json dosyasının içeriğini aşağıdaki gibi düzenleyelim.
{
"name": "blog-mountebank-sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"mock": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"mountebank": "^2.2.1",
"node-fetch": "^2.6.0"
}
}
Artık yaptıklarımız test edebiliriz. Mountebank'i ayağa kaldırmak için terminalimize aşağıdaki komutu yazalım.
yarn run mock
Bu komutu yazdıktan sonra aşağıdaki gibi bir ekranla karşılaşmalıyız.
Komut başarıyla çalıştığında Mountebank sunucusunun 4001 portundan, imposter'ın ise 4006 portundan yayın yaptığı gözükmektedir.
Imposter'ın başarıyla kayıt edilip edilmediğini anlayabilmenin farklı bir yolu ise http://localhost:4001/imposters adresini tarayıcımızda açmaktır. Bu adrese gittiğimiz zaman ise aşağıdakine benzer bir ekran bizi karşılayacaktır.
Bu ekrandaki http:4006 bağlantısına tıkladığımızda ise imposter'ın içinde tanımlı olan stub bilgisine de arayüz üzerinden ulaşılabilir.
Şimdi Postman veya benzeri bir uygulama ile yapmış olduklarımızı test edelim.
Postman'i açıp http://localhost:4006/users
adresine GET
isteği atalım. Herhangi bir sorun yoksa aşağıdaki gibi bir ekranla karşılaşacağız.
Görüldüğü gibi stub dosyamızdaki ilk tanımlamaya göre cevabın döndüğünü görüyoruz.
Stub dosyamızdaki ikinci tanımlamaya göre isteğimizi atıp test etmeye devam edelim. Bu işlem için http://localhost:4006/users/8006A836-FB25-4801-BB71-57C9E6A05065
adresine POST
isteği atalım. Bu istek sonucunda aşağıdaki gibi bir ekranla karşılaşacağız.
Görüldüğü gibi Mountebank attığımız istek sonucunda stub dosyasında tanımlı olan body bilgisini ve 404 HTTP durum kodunu dönmüştür.
CI & CD Süreçlerinde Mountbank'in Ayarlanması
Mountebank'i CI & CD süreçlerinizde testlerinizin ihtiyaç duyduğu veriler için kullanabilirsiniz. Bu süreci başlatabilmek için yarn run mock
veya npm run mock
komutunu sürecinize eklemeniz yeterlidir. Ancak bazı durumlarda kullanılan CI aracı Mountebank instance'ını kapatamayabiliyor. Bunu önlemek adına 4001 portundan çalışan Mountebank instance'ını işlem sonunda kapatacak şekilde bir komut yazmak gerekmektedir.
Bu işlem için kill-port paketini kullanacağım. Aslında bu paketin yaptığı tek iş Windows işletim sisteminde bu işlem yapılıyorsa TaskKill
, Linux veya MacOS işletim sisteminde bu işlem yapılıyorsa lsof
ve kill
komutlarını çalıştırmaktadır. https://github.com/tiaanduplessis/kill-port/blob/master/index.js adresinden de yaptığı işi inceleyebilir, paket kullanmak istemiyorsanız projenizde gerekli düzenlemeyi yapabilirsiniz. Bu paketi kullanıyorsanız CI sürecinizde kill-port 4001
komutunu yazmanız yeterlidir.
Port üzerinden ilgili Mountebank işlemine ulaşıp kapatmak yerine elinizde Mountebank'in pid(Process Id) bilgisi varsa kill -9 <pid>
komutunu kullanarak da ilgili Mountebank işlemini kapatabilirsiniz.
Bu yazıda yapılan örnek uygulamaya https://github.com/mennan/blog-mountebank-sample adresinden ulaşabilirsiniz.