Teknik Pengujian Terbaik dalam JavaScript dan Node.js


Ini adalah panduan komprehensif untuk memberikan keandalan dalam JavaScript dan Node.js. Di sini dikumpulkan puluhan pos, buku, dan alat terbaik.

Pertama, berurusan dengan metode pengujian yang diterima secara umum yang mendasari aplikasi apa pun. Dan kemudian Anda dapat mempelajari bidang yang menarik bagi Anda: frontend dan antarmuka, backend, CI atau semua hal di atas.

Isi



Bagian 0. Aturan Emas


0. Golden Rule: Stick to Lean Testing


Apa yang harus dilakukan Kode uji berbeda dari apa yang beroperasi. Jadikan sesederhana mungkin, pendek, bebas dari abstraksi, lajang, hebat di tempat kerja dan hemat. Orang lain harus melihat tes dan segera mengerti apa yang dia lakukan.

Kepala kami sibuk dengan kode produksi, mereka tidak memiliki ruang bebas untuk kompleksitas tambahan. Jika kita memasukkan bagian baru kode kompleks ke dalam pikiran kita yang buruk, ini akan memperlambat pekerjaan seluruh tim pada tugas, untuk kepentingan yang sedang kita uji. Bahkan, karena ini, banyak tim hanya menghindari tes.

Tes - ini adalah kesempatan untuk mendapatkan asisten yang ramah dan tersenyum, dengan siapa sangat baik untuk bekerja dan yang memberikan pengembalian besar pada investasi kecil. Para ilmuwan percaya bahwa di otak kita ada dua sistem: satu untuk tindakan yang tidak membutuhkan usaha, seperti mengemudi di jalan kosong, dan yang kedua untuk operasi kompleks yang membutuhkan kesadaran, seperti menyelesaikan persamaan matematika. Buat tes Anda untuk sistem pertama, sehingga ketika Anda melihat kode Anda mendapatkan perasaan kesederhanaan yang sebanding dengan mengedit dokumen HTML, dan bukan dengan solusi 2X(17 × 24) .

Ini dapat dicapai dengan pemilihan metode, alat, dan tujuan pengujian yang cermat, sehingga ekonomis dan memberikan ROI yang besar. Uji sebanyak yang diperlukan, cobalah bersikap fleksibel. Terkadang ada baiknya membuang beberapa tes dan mengorbankan keandalan demi kecepatan dan kesederhanaan.



Sebagian besar rekomendasi di bawah ini berasal dari prinsip ini.
Apakah kamu siap

Bagian 1. Anatomi ujian


1.1 Nama setiap tes harus terdiri dari tiga bagian


Apa yang harus dilakukan Laporan pengujian harus menunjukkan apakah revisi aplikasi saat ini memenuhi persyaratan dari orang-orang yang tidak terbiasa dengan kode: penguji yang terlibat dalam penyebaran insinyur DevOps, serta diri Anda dalam dua tahun. Akan lebih baik jika tes melaporkan informasi dalam bahasa persyaratan, dan namanya terdiri dari tiga bagian:

  1. Apa sebenarnya yang sedang diuji? Misalnya, metode ProductsService.addNewProduct .
  2. Dalam kondisi dan skenario apa? Misalnya, harga tidak diteruskan ke metode.
  3. Apa hasil yang diharapkan? Misalnya, produk baru tidak disetujui.

Jika tidak. Penyebaran gagal, tes yang disebut "Tambah produk" gagal. Apakah Anda mengerti apa yang salah?

Catatan Setiap bab memiliki kode contoh, dan terkadang ilustrasi. Lihat spoiler.

Contoh kode
Bagaimana cara melakukannya dengan benar. Nama tes terdiri dari tiga bagian.

 //1. unit under test describe('Products Service', function() { describe('Add new product', function() { //2. scenario and 3. expectation it('When no price is specified, then the product status is pending approval', ()=> { const newProduct = new ProductService().add(...); expect(newProduct.status).to.equal('pendingApproval'); }); }); }); 


1.2 Struktur tes sesuai dengan pola AAA


Apa yang harus dilakukan Setiap tes harus terdiri dari tiga bagian yang dipisahkan dengan jelas: Atur (persiapan), Tindakan (tindakan) dan Tegas (hasil). Mematuhi struktur seperti itu memastikan bahwa pembaca kode Anda tidak harus menggunakan prosesor otak untuk memahami rencana pengujian:

Atur: semua kode yang membawa sistem untuk menyatakan sesuai dengan skenario pengujian. Ini mungkin termasuk membuat instance dari modul dalam perancang tes, menambahkan catatan ke database, membuat bertopik bukan objek, dan kode lain yang menyiapkan sistem untuk menjalankan tes.

Act: eksekusi kode sebagai bagian dari tes. Biasanya hanya satu baris.
Menegaskan: pastikan bahwa nilai yang diperoleh memenuhi harapan. Biasanya hanya satu baris.

Jika tidak. Anda tidak hanya akan menghabiskan berjam-jam bekerja dengan kode utama, tetapi otak Anda akan membengkak juga dari apa yang seharusnya menjadi pekerjaan sederhana - dari pengujian.

Contoh kode
Bagaimana cara melakukannya dengan benar. Tes terstruktur sesuai dengan pola AAA.

 describe.skip('Customer classifier', () => { test('When customer spent more than 500$, should be classified as premium', () => { //Arrange const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); //Act const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); //Assert expect(receivedClassification).toMatch('premium'); }); }); 

Contoh dari antipattern. Tidak ada pemisahan, dalam satu bagian, lebih sulit untuk ditafsirkan.

 test('Should be classified as premium', () => { const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); expect(receivedClassification).toMatch('premium'); }); 


1.3 Jelaskan harapan dalam bahasa produk: sebutkan dengan gaya BDD


Apa yang harus dilakukan Tes pemrograman dalam gaya deklaratif memungkinkan pengguna untuk segera memahami esensi tanpa menghabiskan siklus prosesor otak tunggal. Ketika Anda menulis kode imperatif yang dikemas dalam logika kondisional, pembaca harus berusaha keras. Dari sudut pandang ini, Anda perlu menggambarkan harapan dalam bahasa mirip manusia dalam gaya BDD deklaratif menggunakan ekspektasi / seharusnya dan tidak menggunakan kode khusus. Jika di Chai dan Jest tidak ada pernyataan yang perlu, yang sering diulang, maka Anda dapat memperluas Matcher Jest atau menulis plugin Anda sendiri untuk Chai .

Jika tidak. Tim akan menulis lebih sedikit tes dan menghias tes yang mengganggu with .skip() .

Contoh kode
Contoh menggunakan Mocha .

Contoh dari antipattern. Untuk memahami esensi dari tes ini, pengguna dipaksa untuk melewati kode imperatif yang agak panjang.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins "admin1", "admin2" and "user1" const allAdmins = getUsers({adminOnly:true}); const admin1Found, adming2Found = false; allAdmins.forEach(aSingleUser => { if(aSingleUser === "user1"){ assert.notEqual(aSingleUser, "user1", "A user was found and not admin"); } if(aSingleUser==="admin1"){ admin1Found = true; } if(aSingleUser==="admin2"){ admin2Found = true; } }); if(!admin1Found || !admin2Found ){ throw new Error("Not all admins were returned"); } }); 

Bagaimana cara melakukannya dengan benar. Membaca tes deklaratif ini sangat mudah.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins const allAdmins = getUsers({adminOnly:true}); expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) .but.not.include.ordered.members(["user1"]); }); 


1.4 Patuhi pengujian kotak hitam: uji hanya metode publik


Apa yang harus dilakukan Menguji bagian dalam akan menyebabkan overhead besar dan hampir tidak akan menghasilkan apa-apa. Jika kode atau API Anda memberikan hasil yang benar, apakah layak untuk menghabiskan tiga jam pengujian BAGAIMANA itu bekerja secara internal dan kemudian mendukung tes rapuh ini? Ketika Anda memeriksa perilaku publik, Anda secara bersamaan secara implisit memeriksa implementasi itu sendiri, pengujian Anda akan gagal hanya jika ada masalah tertentu (misalnya, output yang salah). Pendekatan ini juga disebut pengujian perilaku. Di sisi lain, jika Anda menguji internal (metode "kotak putih"), maka alih-alih merencanakan output komponen, Anda akan fokus pada detail kecil, dan tes Anda mungkin rusak karena perubahan kecil pada kode, bahkan jika hasilnya baik-baik saja, tetapi pengawalan akan membutuhkan lebih banyak sumber daya.

Jika tidak. Tes Anda akan berperilaku seperti anak laki - laki yang berteriak "Serigala!" : Dengan keras melaporkan positif palsu (misalnya, tes gagal karena perubahan nama variabel pribadi). Tidak mengherankan bahwa segera orang akan mulai mengabaikan pemberitahuan CI, dan suatu hari mereka akan kehilangan bug yang sebenarnya ...

Contoh kode
Contoh dari antipattern. menguji bagian dalam tanpa alasan yang bagus.

Contoh menggunakan Mocha .

 class ProductService{ //this method is only used internally //Change this name will make the tests fail calculateVAT(priceWithoutVAT){ return {finalPrice: priceWithoutVAT * 1.2}; //Change the result format or key name above will make the tests fail } //public method getPrice(productId){ const desiredProduct= DB.getProduct(productId); finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; } } it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => { //There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); }); 


1.5 Pilih implementasi simulasi yang tepat: hindari benda palsu yang mendukung stub dan mata-mata


Apa yang harus dilakukan Implementasi yang disimulasikan (tes ganda) adalah kejahatan yang diperlukan karena mereka terkait dengan internal aplikasi, dan beberapa bernilai tinggi ( menyegarkan memori implementasi yang ditiru: benda palsu (tiruan), bertopik (bertopik), dan objek mata-mata (mata-mata) ) Namun, tidak semua teknik itu setara. Mata-mata dan bertopik dirancang untuk menguji persyaratan, tetapi memiliki efek samping yang tak terhindarkan - mereka juga sedikit mempengaruhi bagian dalam. Dan benda-benda palsu dirancang untuk menguji bagian dalam, yang mengarah ke overhead besar, seperti yang dijelaskan dalam bab 1.4.

Sebelum menggunakan implementasi simulasi, tanyakan pada diri sendiri pertanyaan paling sederhana: "Apakah saya menggunakan ini untuk menguji fungsionalitas yang telah muncul atau mungkin muncul dalam dokumentasi dengan persyaratan?" Jika tidak, itu menampar pengujian kotak putih.

Misalnya, jika Anda ingin mengetahui apakah aplikasi berperilaku sebagaimana mestinya ketika layanan pembayaran tidak tersedia, Anda dapat membuat tulisan rintisan dan mengembalikan "Tidak ada jawaban" untuk memeriksa apakah modul yang diuji mengembalikan nilai yang benar. Jadi, Anda dapat memeriksa perilaku / respons / keluaran aplikasi dalam skenario tertentu. Anda juga dapat mengkonfirmasi dengan bantuan mata-mata bahwa ketika layanan tidak tersedia, surat itu dikirim, itu juga pengujian perilaku, yang lebih baik tercermin dalam dokumentasi dengan persyaratan ("Kirim surat jika informasi pembayaran tidak dapat disimpan"). Pada saat yang sama, jika Anda membuat layanan pembayaran palsu dan memastikan bahwa itu disebut menggunakan tipe JS yang benar, maka pengujian Anda ditujukan untuk internal yang tidak terkait dengan fungsi aplikasi dan yang cenderung sering berubah.

Jika tidak. Setiap refactoring kode melibatkan menemukan dan memperbarui semua objek palsu dalam kode. Tes dari asisten teman berubah menjadi beban.

Contoh kode
Contoh dari antipattern. Benda palsu adalah untuk nyali.

Contoh menggunakan Sinon .

 it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => { //Assume we already added a product const dataAccessMock = sinon.mock(DAL); //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false); new ProductService().deletePrice(theProductWeJustAdded); mock.verify(); }); 

Bagaimana cara melakukannya dengan benar. Mata-mata dirancang untuk menguji persyaratan, tetapi ada efek samping - mereka pasti mempengaruhi bagian dalam.

 it("When a valid product is about to be deleted, ensure an email is sent", async () => { //Assume we already added here a product const spy = sinon.spy(Emailer.prototype, "sendEmail"); new ProductService().deletePrice(theProductWeJustAdded); //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) }); 


1.6 Jangan menggunakan "foo", gunakan input realistis


Apa yang harus dilakukan Seringkali bug produksi terjadi dengan data input yang sangat spesifik dan mengejutkan. Semakin realistis data selama pengujian, semakin besar kemungkinan untuk menangkap bug tepat waktu. Untuk menghasilkan data pseudo-nyata yang mensimulasikan variasi dan jenis data produksi, gunakan perpustakaan khusus, misalnya, Faker . Perpustakaan semacam itu dapat menghasilkan nomor telepon yang realistis, nama panggilan pengguna, kartu bank, nama perusahaan, bahkan teks "lorem ipsum". Anda dapat membuat tes (di atas unit test, dan bukan sebaliknya) yang mengacak data palsu agar sesuai dengan modul ke dalam tes, atau bahkan mengimpor data nyata dari lingkungan produksi. Ingin melangkah lebih jauh? Baca bab berikutnya (tentang pengujian berbasis properti).

Jika tidak. Pengujian pengembangan Anda akan terlihat berhasil menggunakan input sintetik seperti "Foo", dan data produksi mungkin macet ketika peretas @3e2ddsf . ##' 1 fdsfds . fds432 AAAA garis rumit seperti @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA .

Contoh kode
Contoh dari antipattern. Rangkaian uji yang berhasil dijalankan karena penggunaan data yang tidak realistis.

Contoh menggunakan Jest .

 const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;//no white-space allowed if(!productNameRegexNoSpace.test(name)) return false;//this path never reached due to dull input //some logic here return true; }; test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { //The string "Foo" which is used in all tests never triggers a false result const addProductResult = addProduct("Foo", 5); expect(addProductResult).to.be.true; //Positive-false: the operation succeeded because we never tried with long //product name including spaces }); 

Bagaimana cara melakukannya dengan benar. Acak input realistis.

 it("Better: When adding new valid product, get successful confirmation", async () => { const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); //Generated random input: {'Sleek Cotton Computer', 85481} expect(addProductResult).to.be.true; //Test failed, the random input triggered some path we never planned for. //We discovered a bug early! }); 


1.7 Gunakan pengujian berbasis properti untuk memvalidasi beberapa kombinasi input


Apa yang harus dilakukan Biasanya untuk setiap tes kami memilih beberapa sampel data input. Bahkan jika format input mirip dengan data nyata (lihat bab “Jangan gunakan“ foo ”), kami hanya mencakup beberapa kombinasi data input (metode ('', true, 1) , metode ("string" , false" , 0) Tetapi dalam operasi, API yang disebut dengan lima parameter dapat dipanggil dengan ribuan kombinasi yang berbeda, salah satunya dapat menyebabkan proses crash ( fuzzing ). Bagaimana jika Anda dapat menulis satu tes yang secara otomatis mengirim 1000 kombinasi data input dan memperbaiki, pada kombinasi apa kode tidak mengembalikan jawaban yang benar? Hal yang sama kita lakukan dengan m todike tes berdasarkan sifat: dengan mengirimkan semua kemungkinan kombinasi dari input data ke dalam unit uji kami meningkatkan kesempatan deteksi bug, misalnya, kita memiliki metode. addNewProduct(id, name, isDiscount) Mendukung perpustakaan akan memanggil metode ini dengan sejumlah kombinasi. (, , ) , misalnya, (1, "iPhone", false) , (2, "Galaxy", true) , dll. Anda dapat menguji berdasarkan properti menggunakan pelari uji favorit Anda (Mocha, Jest dll) dan perpustakaan seperti js-verifikasi atau testcheck (ini memiliki dokumentasi yang jauh lebih baik). Anda juga dapat mencoba perpustakaan periksa cepat , yang menawarkan fitur-fitur tambahan dan secara aktif disertai oleh penulis.

Jika tidak. Anda tanpa berpikir memilih data input untuk pengujian, yang hanya mencakup jalur eksekusi kode yang berfungsi dengan baik. Sayangnya, ini mengurangi efektivitas pengujian sebagai cara mendeteksi kesalahan.

Contoh kode
Bagaimana cara melakukannya dengan benar. Uji banyak kombinasi dengan uji mocha.

 require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => { //this will run 100 times with different random properties check.it('Add new product with random yet valid properties, always successful', gen.int, gen.string, (id, name) => { expect(addNewProduct(id, name).status).to.equal('approved'); }); }) }); 


1.8 Jika perlu, gunakan hanya pemotretan singkat dan inline.


Apa yang harus dilakukan Ketika Anda perlu menguji berdasarkan snapshot , gunakan snapshot pendek tanpa semua tambahan (misalnya, dalam 3-7 baris), termasuk sebagai bagian dari pengujian ( Inline Snapshot ), dan bukan sebagai file eksternal. Mengikuti rekomendasi ini akan membuat tes Anda jelas dan lebih dapat diandalkan.

Di sisi lain, panduan dan alat "snapshot klasik" memprovokasi kami untuk menyimpan file besar (misalnya, markup untuk rendering komponen atau hasil JSON API) di media eksternal dan membandingkan hasilnya dengan versi yang disimpan setiap kali tes dijalankan. Itu bisa, katakanlah, secara implisit mengaitkan pengujian kami dengan 1000 baris yang berisi 3.000 nilai yang tidak pernah dilihat oleh penulis tes yang tidak diharapkannya. Kenapa ini buruk? Karena ada 1000 alasan mengapa tes gagal. Bahkan satu baris dapat membatalkan snapshot, dan ini bisa sering terjadi. Berapa banyak Setelah setiap ruang, komentar, atau perubahan kecil dalam CSS atau HTML. Selain itu, nama tes tidak akan memberi tahu Anda tentang kegagalan, karena hanya memeriksa bahwa 1000 baris tidak berubah, dan juga mendorong penulis tes untuk mengambil selama yang diinginkan dokumen panjang yang ia tidak bisa menganalisis dan memverifikasi. Semua ini adalah gejala dari tes yang tidak jelas dan tergesa-gesa yang tidak memiliki tugas yang jelas dan berusaha untuk mencapai terlalu banyak.

Perlu dicatat bahwa ada beberapa situasi di mana dapat diterima untuk menggunakan gambar panjang dan eksternal, misalnya, ketika mengkonfirmasi skema, dan bukan data (mengekstraksi nilai dan fokus pada bidang), atau ketika dokumen yang diterima jarang berubah.

Jika tidak. Tes UI gagal. Kode terlihat baik, piksel ideal ditampilkan di layar, jadi apa yang terjadi? Pengujian Anda dengan snapshot baru saja mengungkapkan perbedaan antara dokumen asli dan yang baru diterima - karakter spasi ditambahkan ke markup ...

Contoh kode
Contoh dari antipattern. Mengaitkan tes dengan beberapa 2000 baris kode yang tidak diketahui.

 it('TestJavaScript.com is renderd correctly', () => { //Arrange //Act const receivedPage = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert expect(receivedPage).toMatchSnapshot(); //We now implicitly maintain a 2000 lines long document //every additional line break or comment - will break this test }); 

Bagaimana cara melakukannya dengan benar. Harapan terlihat dan menjadi sorotan.

 it('When visiting TestJavaScript.com home page, a menu is displayed', () => { //Arrange //Act receivedPage tree = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert const menu = receivedPage.content.menu; expect(menu).toMatchInlineSnapshot(` <ul> <li>Home</li> <li> About </li> <li> Contact </li> </ul> `); }); 


1.9 Hindari bangku tes global dan data awal, tambahkan data ke setiap tes secara terpisah


Apa yang harus dilakukan Menurut aturan emas (bab 0), setiap tes harus menambah dan bekerja dalam rangkaian barisnya sendiri dalam database untuk menghindari binding, dan lebih mudah bagi pengguna untuk memahami tes. Pada kenyataannya, penguji sering melanggar aturan ini, sebelum menjalankan tes mengisi database dengan data awal (biji) ( juga disebut "bangku tes" ) untuk meningkatkan produktivitas. , (. « »), . . , , (, ).

. , , , ? , , , .

. - .

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

. , .

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


1.10 ,


. , - , try-catch-finally , . ( ), .

Chai: expect(method).to.throw ( Jest: expect(method).toThrow() ). , , . , , .

. (, CI-) , .

. , try-catch .

 /it("When no product name, it throws error 400", async() => { let errorWeExceptFor = null; try { const result = await addNewProduct({name:'nest'});} catch (error) { expect(error.code).to.equal('InvalidInput'); errorWeExceptFor = error; } expect(errorWeExceptFor).not.to.be.null; //if this assertion fails, the tests results/reports will only show //that some value is null, there won't be a word about a missing Exception }); 

. , , , QA .

 it.only("When no product name, it throws error 400", async() => { expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); }); 


1.11


. :

  • smoke-,
  • IO-less,
  • , , ,
  • , pull request', .

, , , #cold #api #sanity. . , Mocha : mocha — grep 'sanity' .

. , , , , , , , .

. '#cold-test' (Cold=== , - , ).

 //this test is fast (no DB) and we're tagging it correspondingly //now the user/CI can run it frequently describe('Order service', function() { describe('Add new order #cold-test #sanity', function() { it('Scenario - no currency was supplied. Expectation - Use the default currency #sanity', function() { //code logic here }); }); }); 


1.12


. , Node.js . , Node.

TDD . , , , . -- , - . , , . , . , , , , (, ..).

. , .

2:


️2.1 :


. 10 , . . , 10 (, , ), , , ? ?

: 2019- , TDD , , , . , , , . IoT-, Kafka RabbitMQ, , - . , , ? (, , ), , - .

( ) , , (« API, , !» (consumer-driven contracts)). , : , , , .

: TDD - . TDD , . , .

. ROI, Fuzz, , 10 .

. Cindy Sridharan 'Testing Microservices — the sane way'



Contoh:



2.2


. , . , . , , ? — . : TDD-, .

«», API, , (, , in-memory ), , , . , , « », .

. , , 20 %.

. Express API ( ).



2.3 , API


. , ( ). - , ! — , , . « -22 » : , , . (consumer-driven contracts) PACT : , … ! PACT — «», . PACT- — . , API CI, .

. — .

.



2.4


. , Express-. . , , , JS- {req,res}. , (, Sinon ) {req,res}, , . node-mock-http {req,res} . , , HTTP-, res- (. ).

. Express- === .

. , Express-.

 //the middleware we want to test const unitUnderTest = require('./middleware') const httpMocks = require('node-mocks-http'); //Jest syntax, equivalent to describe() & it() in Mocha test('A request without authentication header, should return http status 403', () => { const request = httpMocks.createRequest({ method: 'GET', url: '/user/42', headers: { authentication: '' } }); const response = httpMocks.createResponse(); unitUnderTest(request, response); expect(response.statusCode).toBe(403); }); 


2.5


. . CI- , . (, ), (, ), . Sonarqube (2600+ ) Code Climate (1500+ ). :: Keith Holliday

. , .

. CodeClimate, :



2.6 , Node


. , . ( ) . , - , ? ? , API 50 % ? , Netflix - ( Chaos Engineering ). : , . , Netflix, chaos monkey , , , , - ( Kubernetes kube-monkey , ). , . , , Node- , , v8 1,7 , UX , ? node-chaos (-), , Node.

. , production .

. Node-chaos , Node.js, .



2.7 ,


. ( 0), , , . , (seeds) ( « » ) . , (. « »), , . . , , (, ).

. , , , ? , , , .

. - .

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

. , .

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


3:


3.1. UI


. , , , . , , ( HTML CSS) . , (, , , ), , , .

. 10 , 500 (100 = 1 ) - - .

. .

 test('When users-list is flagged to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ { id: 1, name: 'Yoni Goldberg', vip: false }, { id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Extract the data from the UI first const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent); const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name); expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here }); 

. UI .
 test('When flagging to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ {id: 1, name: 'Yoni Goldberg', vip: false }, {id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Mix UI & data in assertion expect(getAllByTestId('user')).toEqual('[<li data-testid="user">John Doe</li>]'); }); 


3.2 HTML- ,


. HTML- , . , , CSS-. , 'test-id-submit-button'. . , , .

. , , . — , , Ajax . . , CSS 'thick-border' 'thin-border'

. , .

 // the markup code (part of React component) <b> <Badge pill className="fixed_badge" variant="dark"> <span data-testid="errorsLabel">{value}</span> <!-- note the attribute data-testid --> </Badge> </b> // this example is using react-testing-library test('Whenever no data is passed to metric, show 0 as default', () => { // Arrange const metricValue = undefined; // Act const { getByTestId } = render(<dashboardMetric value={undefined}/>); expect(getByTestId('errorsLabel')).text()).toBe("0"); }); 

. CSS-.

 <!-- the markup code (part of React component) --> <span id="metric" className="d-flex-column">{value}</span> <!-- what if the designer changes the classs? --> // this exammple is using enzyme test('Whenever no data is passed, error metric shows zero', () => { // ... expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); }); 


3.3


. , , . , , . , — - , (. « » ). (, ) , .

, : , . ( ) . , .

. , . ?

. .

 class Calendar extends React.Component { static defaultProps = {showFilters: false} render() { return ( <div> A filters panel with a button to hide/show filters <FiltersPanel showFilter={showFilters} title='Choose Filters'/> </div> ) } } //Examples use React & Enzyme test('Realistic approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = mount(<Calendar showFilters={false} />) // Act wrapper.find('button').simulate('click'); // Assert expect(wrapper.text().includes('Choose Filter')); // This is how the user will approach this element: by text }) 

. .

 test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = shallow(<Calendar showFilters={false} title='Choose Filter'/>) // Act wrapper.find('filtersPanel').instance().showFilters(); // Tap into the internals, bypass the UI and invoke a method. White-box approach // Assert expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'}); // what if we change the prop name or don't pass anything relevant? }) 


3.4 .


. (, ). (, setTimeOut ) , . (, Cypress cy.request('url') ), API, wait(expect(element)) @testing-library/DOM . , API, , . , , hurry-up the clock . — , , ( ). , , - npm- , , wait-for-expect .

. , . , . .

. E2E API (Cypress).

 // using Cypress cy.get('#show-products').click()// navigate cy.wait('@products')// wait for route to appear // this line will get executed only when the route is ready 

. , DOM- (@testing-library/dom).
 // @testing-library/dom test('movie title appears', async () => { // element is initially not present... // wait for appearance await wait(() => { expect(getByText('the lion king')).toBeInTheDocument() }) // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 

. .

 test('movie title appears', async () => { // element is initially not present... // custom wait logic (caution: simplistic, no timeout) const interval = setInterval(() => { const found = getByText('the lion king'); if(found){ clearInterval(interval); expect(getByText('the lion king')).toBeInTheDocument(); } }, 100); // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 


3.5.


. - , . , , . : pingdom , AWS CloudWatch gcp StackDriver , , SLA. , , (, lighthouse , pagespeed ), . — , : , (TTI) . , , , , , DOM, SSL . , CI, 247 CDN.

. , , , , - CDN.

. Lighthouse .



3.6 API


. ( 2), , , ( ). API (, Sinon , Test doubles ), API. . API , ( ). API, . , , API . , : .

. , API 100 , 20 .

. API-.

 // unit under test export default function ProductsList() { const [products, setProducts] = useState(false) const fetchProducts = async() => { const products = await axios.get('api/products') setProducts(products); } useEffect(() => { fetchProducts(); }, []); return products ? <div>{products}</div> : <div data-testid='no-products-message'>No products</div> } // test test('When no products exist, show the appropriate message', () => { // Arrange nock("api") .get(`/products`) .reply(404); // Act const {getByTestId} = render(<ProductsList/>); // Assert expect(getByTestId('no-products-message')).toBeTruthy(); }); 


3.7 ,


. E2E (end-to-end, ) UI (. 3.6). , , . , , - . , — (, ), . - , , UI- Cypress Pupeteer . , : 50 , , . 10 . , , , — . , .

. UI , , ( , UI) .

3.8


. , API , , . (before-all), - . , : . , . - API- . , . (, ), , , . , : , API (. 3.6).

. , 200 , 100 , 20 .

. (before-all), (before-each) (, Cypress).

Cypress .

 let authenticationToken; // happens before ALL tests run before(() => { cy.request('POST', 'http://localhost:3000/login', { username: Cypress.env('username'), password: Cypress.env('password'), }) .its('body') .then((responseFromLogin) => { authenticationToken = responseFromLogin.token; }) }) // happens before EACH test beforeEach(setUser => () { cy.visit('/home', { onBeforeLoad (win) { win.localStorage.setItem('token', JSON.stringify(authenticationToken)) }, }) }) 


3.9 smoke-,


. production- , , . , , , , . smoke- . production, , , . , smoke- , .

. , , production . /Payment.

. Smoke- .

 it('When doing smoke testing over all page, should load them all successfully', () => { // exemplified using Cypress but can be implemented easily // using any E2E suite cy.visit('https://mysite.com/home'); cy.contains('Home'); cy.contains('https://mysite.com/Login'); cy.contains('Login'); cy.contains('https://mysite.com/About'); cy.contains('About'); }) 


3.10


. , . «» , , , , . , ( ) , , -, , , . « », . . , Cucumber JavaScript . StoryBook UI- , (, , , ..) , . , , .

. , .

. cucumber-js.

 // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate Feature: Twitter new tweet I want to tweet something in Twitter @focus Scenario: Tweeting from the home page Given I open Twitter home Given I click on "New tweet" button Given I type "Hello followers!" in the textbox Given I click on "Submit" button Then I see message "Tweet saved" 

. Storybook , .



3.11


. , . , . , . , - . , . , , , . , - . UI « ». , (, wraith , PhantomCSS), . (, Applitools , Perci.io ) , , « » (, ), DOM/CSS, .

. , ( ) , ?

. : , .



. wraith UI.

 ​# Add as many domains as necessary. Key will act as a label​ domains: english: "http://www.mysite.com"​ ​# Type screen widths below, here are a couple of examples​ screen_widths: - 600​ - 768​ - 1024​ - 1280​ ​# Type page URL paths below, here are a couple of examples​ paths: about: path: /about selector: '.about'​ subscribe: selector: '.subscribe'​ path: /subscribe 


4:


4.1 (~80 %),


. — , . , . — (, ), . ? , 10-30 % . 100 % , . . , : Airbus, ; , 50 % . , , 80 % ( Fowler: «in the upper 80s or 90s» ), , , .

: (CI), ( Jest ) , . , . , ( ) — . , — , , . , .

. . , , . .

.



. ( Jest).



4.2 ,


. , . , , , , . , , - , . PricingCalculator , , , 10 000 … , , . , . 80- , . : , , , . , - .

. , , , .

. ? , QA . : , - . , - API .




4.3


. : 100 %, . Bagaimana bisa begitu? , , , . . - : , , , .

, . JavaScript- Stryker :

  1. « ». , newOrder.price===0 newOrder.price!=0 . «» .
  2. , , : , . , , .

, , , .

. , 85- 85 % .

. 100 %, 0 %.

 function addNewOrder(newOrder) { logger.log(`Adding new order ${newOrder}`); DB.save(newOrder); Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); return {approved: true}; } it("Test addNewOrder, don't use such test names", () => { addNewOrder({asignee: "John@mailer.com",price: 120}); });//Triggers 100% code coverage, but it doesn't check anything 

. Stryker reports, , ().



4.4 -


. ESLint. , eslint-plugin-mocha , ( describe() ), , . eslint-plugin-jest , ( ).

. 90- , , , . , .

. , , .

 describe("Too short description", () => { const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words }); it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite expect("somevalue"); // error:no-assert }); it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests }); 


5: CI


5.1 ,


. — . , . , ( !). , . ( ESLint standard Airbnb ), . , eslint-plugin-chai-expect , . Eslint-plugin-promise ( ). Eslint-plugin-security , DOS-. eslint-plugin-you-dont-need-lodash-underscore , , V8, , Lodash._map(…) .

. , , . Apa yang sedang terjadi , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . Mengapa : (1) -> (2) -> (3) . , , .

, , , , - .

CI- ( , CircleCI local CLI ) . , wallaby , ( ) . npm- package.json, , (, , , ). (non-zero exit code) concurrently . — , npm run quality . githook ( husky ).

. , .

. Npm-, , , .

 "scripts": { "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", "inspect:lint": "eslint .", "inspect:vulnerabilities": "npm audit", "inspect:license": "license-checker --failOn GPLv2", "inspect:complexity": "plato .", "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" }, "husky": { "hooks": { "precommit": "npm run inspect:all", "prepush": "npm run inspect:all" } } 


5.3 production-


. — CI-. . — Docker-compose . (, ) production-. AWS Local AWS-. , serverless AWS SAM Faas-.

Kubernetes CI-, . , « Kubernetes» Minikube MicroK8s , , . « Kubernetes»: CI- (, Codefresh ) Kubernetes-, CI- ; .

. .

: CI-, Kubernetes- (Dynamic-environments Kubernetes )

 deploy: stage: deploy image: registry.gitlab.com/gitlab-examples/kubernetes-deploy script: - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN - kubectl create ns $NAMESPACE - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" - mkdir .generated - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml environment: name: test-for-ci 


5.4


. , , . , 500 , , . , CI- ( Jest , AVA Mocha ) , . CI- (!), . , CLI , , .

. — , .

. Mocha parallel Jest Mocha ( JavaScript Test-Runners Benchmark )



5.5


. , . 10 ? CI- npm- license check plagiarism check ( ), , , Stackoveflow .

. , , .

.
 //install license-checker in your CI environment or also locally npm install -g license-checker //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build license-checker --summary --failOn BSD 



5.6


. , Express, . npm audit , snyk ( ). CI .

. . .

: NPM Audit



5.7


. package-lock.json Yarn npm ( ): . npm install npm update , . , — . , package.json ncu .

, . :

  • CI , , npm outdated npm-check-updates (ncu). .
  • , pull request' .

: ? , ( , eslint-scope ). « »: latest , , (, 1.3.1, — 1.3.8).

. , .

: , , ncu CI-.



5.8 CI-, Node


. , Node . , Node.

  1. . , Jenkins .
  2. Docker.
  3. , . . smoke-, (, , ) .
  4. , , , , , .
  5. , . , -. - ( ).
  6. . .
  7. , , .
  8. (, Docker-).
  9. , . node_modules .

. , .

5.9 : CI-, Node


. , , . Node, CI . , MySQL, Postgres. CI- «», MySQl, Postgres Node. , - (, ). CI, , .

. - ?

: Travis ( CI) Node.

 language: node_js node_js: - "7" - "6" - "5" - "4" install: - npm install script: - npm run test 

Source: https://habr.com/ru/post/id466879/


All Articles