Bagaimana cara menghemat uang pada seorang terapis menggunakan pengembangan berbasis tes

Pernahkah Anda mengalami kondisi ini?

gambar

Saya ingin menunjukkan kepada Anda bagaimana TDD dapat meningkatkan kualitas kode menggunakan contoh spesifik.
Karena semua yang saya temui saat mempelajari masalah ini cukup teoretis.
Kebetulan saya kebetulan menulis dua aplikasi yang hampir identik: satu ditulis dengan gaya klasik, karena saya tidak tahu TDD saat itu, dan yang kedua - hanya menggunakan TDD.

Di bawah ini saya akan menunjukkan di mana perbedaan terbesar.

Secara pribadi, ini penting bagi saya, karena setiap kali seseorang menemukan bug dalam kode saya, saya mendapat nilai minus yang besar untuk harga diri. Ya, saya mengerti bahwa bug itu normal, semua orang menulisnya, tetapi perasaan rendah diri tidak hilang. Juga, dalam proses evolusi layanan, saya kadang-kadang menyadari bahwa saya sendiri menulis satu sehingga gatal tangan saya untuk membuang semuanya dan menulis ulang lagi. Dan bagaimana itu terjadi tidak dapat dipahami. Entah bagaimana semuanya baik-baik saja pada awalnya, tetapi setelah beberapa fitur dan setelah beberapa saat Anda tidak dapat melihat arsitektur tanpa air mata. Meskipun sepertinya setiap langkah perubahan itu logis. Perasaan bahwa saya tidak menyukai produk dari pekerjaan saya sendiri mengalir dengan lancar ke perasaan bahwa programmer itu dari saya, permisi, seperti peluru dari omong kosong.

Ternyata saya bukan satu-satunya dan banyak kolega saya memiliki sensasi serupa. Dan kemudian saya memutuskan bahwa saya akan belajar menulis secara normal, atau sudah waktunya untuk mengubah profesi saya. Saya mencoba pengembangan berbasis tes dalam upaya untuk mengubah sesuatu dalam pendekatan pemrograman saya.

Ke depan, berdasarkan hasil beberapa proyek, saya dapat mengatakan bahwa TDD menyediakan arsitektur yang lebih bersih, tetapi memperlambat pengembangan. Dan itu tidak selalu cocok dan tidak untuk semua orang.

Apa itu TDD lagi


gambar


TDD - pengembangan melalui pengujian. Artikel wiki di sini .
Pendekatan klasik adalah pertama menulis aplikasi, kemudian menutupinya dengan tes.

Pendekatan TDD - pertama kita menulis tes untuk kelas, kemudian implementasi. Kami bergerak melalui level abstraksi - dari yang tertinggi ke yang diterapkan, pada saat yang sama memecah aplikasi ke dalam lapisan kelas, dari mana kami memesan perilaku yang kami butuhkan, bebas dari implementasi tertentu.

Dan jika saya membaca ini untuk pertama kalinya, saya juga tidak akan mengerti apa-apa.
Terlalu banyak kata-kata abstrak: mari kita lihat sebuah contoh.
Kami akan menulis aplikasi semi nyata di Jawa, kami akan menulisnya di TDD, dan saya akan mencoba menunjukkan proses berpikir saya selama proses pengembangan dan pada akhirnya menarik kesimpulan apakah masuk akal untuk menghabiskan waktu pada TDD atau tidak.

Tugas praktis


Misalkan kita sangat beruntung bahwa kita memiliki Kerangka Acuan apa yang perlu kita kembangkan. Biasanya, analis tidak peduli dengan itu, dan terlihat seperti ini:

Penting untuk mengembangkan layanan mikro yang akan menghitung kemungkinan penjualan barang dengan pengiriman berikutnya ke klien di rumah. Informasi tentang fitur ini harus dikirim ke sistem DATA pihak ketiga.

Logika bisnis adalah sebagai berikut: barang tersedia untuk dijual dengan pengiriman jika:

  • Produk dalam persediaan
  • Kontraktor (misalnya, perusahaan DostavchenKO) memiliki kesempatan untuk membawanya ke klien
  • Warna produk - bukan biru (kami tidak suka biru)

Layanan microser kami akan diberitahu tentang perubahan dalam jumlah barang di rak toko melalui permintaan-http.

Pemberitahuan ini merupakan pemicu untuk menghitung ketersediaan.

Plus, agar hidup tidak tampak seperti madu:

  • Pengguna harus dapat secara manual menonaktifkan produk tertentu.
  • Agar tidak melakukan spam DATA, Anda hanya perlu mengirim data ketersediaan untuk produk-produk yang telah berubah.

Kami membaca beberapa kali TK - dan pergi.



Tes integrasi


Dalam TDD, salah satu pertanyaan paling penting yang harus Anda tanyakan kepada semua yang Anda tulis adalah: "Apa yang saya inginkan dari ...?"

Dan pertanyaan pertama yang kami tanyakan hanya untuk keseluruhan aplikasi.
Jadi pertanyaannya adalah:

Apa yang saya inginkan dari microservice saya?

Jawabannya adalah:

Sebenarnya banyak hal. Bahkan logika sederhana semacam itu memberikan banyak pilihan, upaya untuk menuliskannya, dan terlebih lagi untuk membuat tes untuk semuanya, bisa menjadi tugas yang mustahil. Oleh karena itu, untuk menjawab pertanyaan di tingkat aplikasi, kami hanya akan memilih kasus uji utama.

Artinya, kami menganggap bahwa semua data input adalah format yang valid, sistem pihak ketiga merespons secara normal, dan sebelumnya tidak ada informasi tentang produk.

Jadi, saya ingin:

  • Suatu peristiwa telah tiba bahwa tidak ada produk di rak. Beritahu bahwa pengiriman tidak tersedia.
  • Acara datang bahwa produk kuning ada dalam stok, DostavchenKO siap untuk mengambilnya. Beri tahu tentang ketersediaan barang.
  • Dua pesan datang berturut-turut - keduanya dengan jumlah barang positif di toko. Terkirim hanya satu pesan.
  • Dua pesan tiba: di pertama ada produk di toko, di kedua - tidak lagi. Kami mengirim dua pesan: pertama - tersedia, lalu - tidak.
  • Saya dapat menonaktifkan produk secara manual, dan pemberitahuan tidak lagi dikirim.
  • ...

Hal utama di sini adalah berhenti tepat waktu: seperti yang sudah saya tulis, ada terlalu banyak pilihan, dan tidak masuk akal untuk menggambarkan semuanya di sini - hanya yang paling mendasar. Di masa depan, ketika kami menulis tes untuk logika bisnis, kombinasi mereka kemungkinan akan mencakup semua yang kami temukan di sini. Motivasi utama di sini adalah untuk memastikan bahwa jika tes ini lulus, maka aplikasi berfungsi sebagaimana yang kita butuhkan.

Semua Wishlist ini sekarang akan kita saring menjadi tes. Selain itu, karena ini adalah Wishlist di level aplikasi, kami akan melakukan tes dengan meningkatkan konteks pegas, yaitu, cukup berat.
Dan ini, sayangnya, untuk banyak ujung TDD, karena untuk menulis tes integrasi seperti itu, Anda perlu cukup banyak upaya sehingga orang tidak selalu mau menghabiskan. Dan ya, ini adalah langkah yang paling sulit, tetapi, percayalah, setelah Anda melewatinya, kode akan hampir menulis sendiri, dan Anda akan yakin bahwa aplikasi Anda akan bekerja seperti yang Anda inginkan.


Dalam proses menjawab pertanyaan, Anda sudah dapat mulai menulis kode di kelas initializr spring yang dihasilkan. Nama uji hanyalah Daftar Keinginan kami. Untuk saat ini, cukup buat metode kosong:

@Test public void notifyNotAvailableIfProductQuantityIsZero() {} @Test public void notifyAvailableYellowProductIfPositiveQuantityAndDostavchenkoApproved() {} @Test public void notifyOnceOnSeveralEqualProductMessages() {} @Test public void notifyFirstAvailableThenNotIfProductQuantityMovedFromPositiveToZero() {} @Test public void noNotificationOnDisabledProduct() {} 

Mengenai penamaan metode: Saya sangat menyarankan Anda untuk membuat mereka informatif, daripada test1 (), test2 (), karena nanti, ketika Anda lupa kelas apa yang Anda tulis dan apa yang bertanggung jawab, Anda akan memiliki kesempatan alih-alih coba parsing langsung kode, cukup buka tes dan baca metode kontrak yang memuaskan kelas.

Mulailah mengisi tes


Gagasan utamanya adalah meniru semua yang eksternal untuk memeriksa apa yang terjadi di dalam.

"Eksternal" sehubungan dengan layanan kami adalah semua yang BUKAN layanan microser itu sendiri, tetapi yang secara langsung berkomunikasi dengannya.

Dalam hal ini, eksternal adalah:

  • Sistem yang akan diberitahukan oleh layanan kami tentang perubahan kuantitas
  • Pelanggan yang akan memutuskan barang secara manual
  • Sistem DostavchenKO pihak ketiga

Untuk meniru permintaan dari dua yang pertama, kami menggunakan springing MockMvc.
Untuk meniru DostavchenKO, kami menggunakan wiremock atau MockRestServiceServer.

Hasilnya, tes integrasi kami terlihat seperti ini:

Tes integrasi
 @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureWireMock(port = 8090) public class TddExampleApplicationTests { @Autowired private MockMvc mockMvc; @Before public void init() { WireMock.reset(); } @Test public void notifyNotAvailableIfProductQuantityIsZero() throws Exception { stubNotification( // language=JSON "{\n" + " \"productId\": 111,\n" + " \"available\": false\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 111,\n" + " \"color\" : \"red\", \n" + " \"productQuantity\": 0\n" + "}"); verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyAvailableYellowProductIfPositiveQuantityAndDostavchenkoApproved() throws Exception { stubDostavchenko("112"); stubNotification( // language=JSON "{\n" + " \"productId\": 112,\n" + " \"available\": true\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 112,\n" + " \"color\" : \"Yellow\", \n" + " \"productQuantity\": 10\n" + "}"); verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyOnceOnSeveralEqualProductMessages() throws Exception { stubDostavchenko("113"); stubNotification( // language=JSON "{\n" + " \"productId\": 113,\n" + " \"available\": true\n" + "}"); for (int i = 0; i < 5; i++) { performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 113,\n" + " \"color\" : \"Yellow\", \n" + " \"productQuantity\": 10\n" + "}"); } verify(1, postRequestedFor(urlEqualTo("/notify"))); } @Test public void notifyFirstAvailableThenNotIfProductQuantityMovedFromPositiveToZero() throws Exception { stubDostavchenko("114"); stubNotification( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"available\": true\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": 10\n" + "}"); stubNotification( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"available\": false\n" + "}"); performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 114,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": 0\n" + "}"); verify(2, postRequestedFor(urlEqualTo("/notify"))); } @Test public void noNotificationOnDisabledProduct() throws Exception { stubNotification( // language=JSON "{\n" + " \"productId\": 115,\n" + " \"available\": false\n" + "}"); disableProduct(115); for (int i = 0; i < 5; i++) { performQuantityUpdateRequest( // language=JSON "{\n" + " \"productId\": 115,\n" + " \"color\" : \"Yellow\",\n" + " \"productQuantity\": " + i + "\n" + "}"); } verify(1, postRequestedFor(urlEqualTo("/notify"))); } private void disableProduct(int productId) throws Exception { mockMvc.perform( post("/disableProduct?productId=" + productId) ).andDo( print() ).andExpect( status().isOk() ); } private void performQuantityUpdateRequest(String content) throws Exception { mockMvc.perform( post("/product-quantity-update") .contentType(MediaType.APPLICATION_JSON) .content(content) ).andDo( print() ).andExpect( status().isOk() ); } private void stubNotification(String content) { stubFor(WireMock.post(urlEqualTo("/notify")) .withHeader("Content-Type", equalTo(MediaType.APPLICATION_JSON_UTF8_VALUE)) .withRequestBody(equalToJson(content)) .willReturn(aResponse().withStatus(HttpStatus.OK_200))); } private void stubDostavchenko(final String productId) { stubFor(get(urlEqualTo("/isDeliveryAvailable?productId=" + productId)) .willReturn(aResponse().withStatus(HttpStatus.OK_200).withBody("true"))); } } 

Apa yang baru saja terjadi?


Kami menulis tes integrasi, yang bagiannya menjamin kami pengoperasian sistem berdasarkan cerita pengguna utama. Dan kami melakukannya SEBELUM kami mulai mengimplementasikan layanan.

Salah satu keuntungan dari pendekatan ini adalah bahwa selama proses penulisan saya harus pergi ke DostavchenKO nyata dan mendapatkan jawaban nyata dari sana untuk permintaan nyata yang kami buat di rintisan kami. Sangat baik bahwa kami mengurus ini pada awal pengembangan, dan tidak setelah semua kode ditulis. Dan di sini ternyata formatnya bukan yang ditentukan dalam TOR, atau layanan umumnya tidak tersedia, atau yang lainnya.

Saya juga ingin mencatat bahwa kita tidak hanya tidak menulis satu baris kode pun yang nantinya akan masuk ke prod, tetapi kita bahkan belum membuat asumsi tunggal tentang bagaimana microservice kita akan diatur di dalam: lapisan apa yang akan ada, apakah akan ada kita menggunakan basis, jika demikian, yang mana, dll. Pada saat menulis tes, kita diabstraksi dari implementasinya, dan, seperti yang akan kita lihat nanti, ini dapat memberikan sejumlah keunggulan arsitektur.

Berbeda dengan TDD kanonik, di mana implementasinya ditulis segera setelah pengujian, uji integrasi tidak akan memakan waktu lama. Bahkan, itu tidak akan berubah hijau sampai akhir pengembangan, sampai semuanya ditulis, termasuk file.
Kita melangkah lebih jauh.

Pengendali


Setelah kami menulis tes integrasi dan sekarang yakin bahwa setelah kami menyelesaikan tugas, kami dapat tidur nyenyak di malam hari, saatnya untuk mulai memprogram lapisan-lapisannya. Dan layer pertama yang akan kita implementasikan adalah controller. Kenapa tepatnya dia? Karena ini adalah titik masuk ke program. Kita perlu bergerak dari atas ke bawah, dari lapisan pertama yang dengannya pengguna akan berinteraksi, ke yang terakhir.
Ini penting.

Dan lagi, semuanya dimulai dengan pertanyaan yang sama:

Apa yang saya inginkan dari controller?

Jawabannya adalah:

Kita tahu bahwa pengontrol terlibat dalam komunikasi dengan pengguna, validasi dan konversi data input dan tidak mengandung logika bisnis. Jadi jawaban untuk pertanyaan ini mungkin seperti ini:

Saya ingin:

  • BAD_REQUEST kembali ke pengguna saat mencoba memutuskan koneksi produk dengan id yang tidak valid
  • BAD_REQUEST ketika mencoba memberi tahu tentang penggantian barang dengan id yang tidak valid
  • BAD_REQUEST ketika mencoba memberi tahu kuantitas negatif
  • INTERNAL_SERVER_ERROR jika DostavchenKO tidak tersedia
  • INTERNAL_SERVER_ERROR, jika tidak dapat mengirim ke DATA

Karena kami ingin menjadi teman pengguna, untuk semua item di atas, selain kode http, Anda perlu menampilkan pesan khusus yang menjelaskan masalah sehingga pengguna mengerti apa masalahnya.

  • 200 jika pemrosesan berhasil
  • INTERNAL_SERVER_ERROR dengan pesan default dalam semua kasus lainnya, agar tidak menyinari stackrace

Sampai saya mulai menulis di TDD, hal terakhir yang saya pikirkan adalah apa yang akan dibawa oleh sistem saya untuk pengguna dalam beberapa kasus khusus dan, pada pandangan pertama, tidak mungkin. Saya tidak berpikir untuk satu alasan sederhana - menulis implementasi sangat sulit, untuk memperhitungkan sepenuhnya semua kasus tepi, kadang-kadang tidak ada cukup RAM di otak. Dan setelah implementasi tertulis, menganalisis kode untuk sesuatu yang mungkin tidak Anda pertimbangkan sebelumnya masih menyenangkan: kita semua berpikir bahwa kita sedang menulis kode yang sempurna segera). Meskipun tidak ada implementasi, tidak perlu memikirkannya, dan tidak ada rasa sakit untuk mengubahnya, jika itu. Setelah menulis tes terlebih dahulu, Anda tidak perlu menunggu sampai bintang-bintang bertemu, dan setelah penarikan ke prod, sejumlah sistem akan gagal, dan pelanggan akan datang berlari kepada Anda dengan permintaan untuk memperbaiki sesuatu. Dan ini tidak hanya berlaku untuk pengontrol.

Mulai tes menulis


Semuanya jelas dengan tiga yang pertama: kami menggunakan validasi pegas, jika permintaan yang tidak valid tiba, aplikasi akan melempar pengecualian, yang akan kami tangkap dalam penangan pengecualian. Di sini, seperti yang mereka katakan, semuanya bekerja dengan sendirinya, tetapi bagaimana controller tahu bahwa beberapa sistem pihak ketiga tidak tersedia?

Jelas bahwa controller itu sendiri tidak boleh tahu apa-apa tentang sistem pihak ketiga, karena sistem apa yang harus ditanyakan dan apa logika bisnisnya, yaitu harus ada semacam perantara. Perantara ini adalah layanan. Dan kami akan menulis tes pada pengontrol menggunakan tiruan dari layanan ini, meniru perilakunya dalam kasus tertentu. Jadi, layanan entah bagaimana harus memberi tahu pengontrol bahwa sistem tidak tersedia. Anda dapat melakukan ini dengan cara yang berbeda, tetapi cara termudah untuk membuang eksekusi kustom. Kami akan menulis tes untuk perilaku pengontrol ini.

Tes untuk kesalahan komunikasi dengan sistem DATA pihak ketiga
 @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @MockBean private UpdateProcessorService updateProcessorService; @Test public void returnServerErrorOnDataCommunicationError() throws Exception { doThrow(new DataCommunicationException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Can't communicate with Data system\"\n" + " }\n" + " ]\n" + "}") ); } } 


Pada tahap ini, beberapa hal muncul sendiri:

  • Suatu layanan yang akan disuntikkan ke controller dan yang akan didelegasikan pemrosesan pesan masuk untuk jumlah barang baru.
  • Metode layanan ini, dan sesuai tanda tangannya, yang akan melakukan pemrosesan ini.
  • Kesadaran bahwa metode tersebut harus membuang eksekusi kustom ketika sistem tidak tersedia.
  • Eksekusi kustom ini sendiri.

Kenapa sendiri? Karena, seperti yang Anda ingat, kami belum menulis implementasi. Dan semua entitas ini muncul dalam proses bagaimana kami memprogram pengujian. Agar kompiler tidak bersumpah, dalam kode nyata, kita harus membuat semua yang dijelaskan di atas. Untungnya, hampir semua IDE akan membantu kami menghasilkan entitas yang diperlukan. Jadi, kami semacam menulis tes - dan aplikasi diisi dengan kelas dan metode.

Secara total, tes untuk pengontrol adalah sebagai berikut:

Tes
 @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @InjectMocks private Controller controller; @MockBean private UpdateProcessorService updateProcessorService; @Autowired private MockMvc mvc; @Test public void returnBadRequestOnDisableWithInvalidProductId() throws Exception { mvc.perform( post("/disableProduct?productId=-443") ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json(getInvalidProductIdJsonContent()) ); } @Test public void returnBadRequestOnNotifyWithInvalidProductId() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": -1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 0\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json(getInvalidProductIdJsonContent()) ); } @Test public void returnBadRequestOnNotifyWithNegativeProductQuantity() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": -10\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"productQuantity is invalid\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnServerErrorOnDostavchenkoCommunicationError() throws Exception { doThrow(new DostavchenkoException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"DostavchenKO communication exception\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnServerErrorOnDataCommunicationError() throws Exception { doThrow(new DataCommunicationException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Can't communicate with Data system\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void return200OnSuccess() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isOk() ); } @Test public void returnServerErrorOnUnexpectedException() throws Exception { doThrow(new RuntimeException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate( //language=JSON "{\n" + " \"productId\": 1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": 10\n" + "}" ).andDo( print() ).andExpect( status().isInternalServerError() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"Internal Server Error\"\n" + " }\n" + " ]\n" + "}") ); } @Test public void returnTwoErrorMessagesOnInvalidProductIdAndNegativeQuantity() throws Exception { performUpdate( //language=JSON "{\n" + " \"productId\": -1,\n" + " \"color\": \"red\",\n" + " \"productQuantity\": -10\n" + "}" ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json("{\n" + " \"errors\": [\n" + " { \"message\": \"productQuantity is invalid\" },\n" + " { \"message\": \"productId is invalid\" }\n" + " ]\n" + "}") ); } private ResultActions performUpdate(String jsonContent) throws Exception { return mvc.perform( post("/product-quantity-update") .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) .content(jsonContent) ); } private String getInvalidProductIdJsonContent() { return //language=JSON "{\n" + " \"errors\": [\n" + " {\n" + " \"message\": \"productId is invalid\"\n" + " }\n" + " ]\n" + "}"; } } 

Sekarang kita dapat menulis implementasi dan memastikan bahwa semua tes berhasil lulus:
Implementasi
 @RestController @AllArgsConstructor @Validated @Slf4j public class Controller { private final UpdateProcessorService updateProcessorService; @PostMapping("/product-quantity-update") public void updateQuantity(@RequestBody @Valid Update update) { updateProcessorService.processUpdate(update); } @PostMapping("/disableProduct") public void disableProduct(@RequestParam("productId") @Min(0) Long productId) { updateProcessorService.disableProduct(Long.valueOf(productId)); } } 


Penangan pengecualian
 @ControllerAdvice @Slf4j public class ApplicationExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse onConstraintViolationException(ConstraintViolationException exception) { log.info("Constraint Violation", exception); return new ErrorResponse(exception.getConstraintViolations().stream() .map(constraintViolation -> new ErrorResponse.Message( ((PathImpl) constraintViolation.getPropertyPath()).getLeafNode().toString() + " is invalid")) .collect(Collectors.toList())); } @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody @ResponseStatus(value = HttpStatus.BAD_REQUEST) public ErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException exception) { log.info(exception.getMessage()); List<ErrorResponse.Message> fieldErrors = exception.getBindingResult().getFieldErrors().stream() .map(fieldError -> new ErrorResponse.Message(fieldError.getField() + " is invalid")) .collect(Collectors.toList()); return new ErrorResponse(fieldErrors); } @ExceptionHandler(DostavchenkoException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDostavchenkoCommunicationException(DostavchenkoException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("DostavchenKO communication exception"))); } @ExceptionHandler(DataCommunicationException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDataCommunicationException(DataCommunicationException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("Can't communicate with Data system"))); } @ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onException(Exception exception) { log.error("Error while processing", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()))); } } 


Apa yang baru saja terjadi?


Di TDD, Anda tidak harus menyimpan semua kode di kepala Anda.

Mari kita lagi: jangan menyimpan seluruh arsitektur dalam RAM. Lihat saja satu lapisan. Dia sederhana.

Dalam proses yang biasa, otak tidak cukup, karena ada banyak implementasi. Jika Anda seorang pahlawan super yang dapat memperhitungkan semua nuansa proyek besar di kepala Anda, maka TDD tidak perlu. Saya tidak tahu caranya. Semakin besar proyek, semakin saya salah.

Setelah menyadari bahwa Anda hanya perlu memahami apa yang dibutuhkan lapisan berikutnya, pencerahan datang dalam kehidupan. Faktanya adalah bahwa pendekatan ini memungkinkan Anda untuk tidak melakukan hal-hal yang tidak perlu. Di sini Anda berbicara dengan seorang gadis. Dia mengatakan sesuatu tentang masalah di tempat kerja. Dan Anda berpikir bagaimana menyelesaikannya, Anda memutar otak Anda. Dan dia tidak perlu menyelesaikannya, dia hanya perlu mengatakannya. Dan itu dia. Dia hanya ingin berbagi sesuatu. Mempelajari hal ini pada tahap mendengarkan pertama () sangat berharga. Untuk yang lainnya ... yah, Anda tahu.


Layanan


Selanjutnya kami mengimplementasikan layanan.

Apa yang kita inginkan dari layanan ini?

Kami ingin dia berurusan dengan logika bisnis, yaitu .:

  1. Dia tahu cara melepaskan barang, dan juga memberi tahu tentang :
  2. Ketersediaan, jika produk tidak terputus, tersedia, warna produk adalah kuning, dan DostavchenKO siap melakukan pengiriman.
  3. Tidak dapat diaksesnya, jika barang tidak tersedia terlepas dari apa pun.
  4. Tidak dapat diaksesnya, jika produk berwarna biru.
  5. Tidak dapat diaksesnya jika DostavchenKO menolak untuk membawanya.
  6. Tidak dapat diaksesnya jika barang terputus secara manual.
  7. Selanjutnya, kami ingin layanan untuk melakukan eksekusi jika salah satu sistem tidak tersedia.
  8. Dan juga, agar tidak melakukan spam DATA, Anda perlu mengatur pengiriman pesan malas, yaitu:
  9. Jika dulu kami mengirim barang yang tersedia untuk barang dan sekarang kami telah menghitung apa yang tersedia, maka kami tidak mengirim apa pun.
  10. Dan jika itu tidak tersedia sebelumnya, tetapi sekarang tersedia, kami mengirimkannya.
  11. Dan Anda perlu menuliskannya di suatu tempat ...

BERHENTI!


Tidakkah Anda berpikir bahwa layanan kami mulai melakukan terlalu banyak?

Menilai dari Daftar Keinginan kami, ia tahu cara mematikan barang, dan mempertimbangkan aksesibilitas, dan memastikan bahwa ia tidak mengirim pesan yang dikirim sebelumnya. Ini bukan kohesi yang tinggi. Hal ini diperlukan untuk memindahkan fungsionalitas yang heterogen ke dalam kelas yang berbeda, dan oleh karena itu harus sudah ada tiga layanan: satu akan menangani pemutusan barang, yang lain akan menghitung kemungkinan pengiriman dan meneruskannya ke layanan yang akan memutuskan apakah akan mengirimnya atau tidak. Omong-omong, dengan cara ini, layanan logika bisnis tidak akan tahu apa-apa tentang sistem DATA, yang juga merupakan nilai tambah yang pasti.

Dalam pengalaman saya, cukup sering, setelah langsung menuju implementasi, mudah untuk mengabaikan momen arsitektur. Jika kami segera menulis layanan, tanpa memikirkan apa yang harus dilakukan, dan, yang lebih penting, daripada TIDAK, kemungkinan tumpang tindih bidang tanggung jawab akan meningkat. Saya ingin menambahkan atas nama saya sendiri bahwa contoh inilah yang terjadi pada saya dalam praktik nyata dan perbedaan kualitatif antara hasil TDD dan pendekatan pemrograman sekuensial yang mengilhami saya untuk menulis posting ini.

Logika bisnis


Berpikir tentang layanan logika bisnis untuk alasan yang sama dengan kohesi tinggi, kami memahami bahwa kami membutuhkan satu tingkat abstraksi lagi antara itu dan DostavchenKO yang sebenarnya. Dan, karena kami merancang layanan terlebih dahulu , kami dapat meminta dari klien DostavchenKO kontrak internal yang kami inginkan. Dalam proses penulisan tes untuk logika bisnis, kami akan memahami apa yang kami inginkan dari klien tanda tangan berikut:

 public boolean isAvailableForTransportation(Long productId) {...} 

Di tingkat layanan, tidak masalah bagi kami bagaimana jawaban DostavchenKO yang sebenarnya: di masa depan, tugas klien entah bagaimana akan mendapatkan informasi ini darinya. Setelah itu mungkin sederhana, tetapi kadang-kadang akan perlu untuk membuat beberapa permintaan: saat ini kita disarikan dari ini.

Kami ingin tanda tangan serupa dari layanan yang akan menangani barang yang terputus:

 public boolean isProductEnabled(Long productId) {...} 

Jadi, pertanyaan "Apa yang saya inginkan dari layanan logika bisnis?" Tercatat dalam tes terlihat sebagai berikut:

Tes Layanan
 @RunWith(MockitoJUnitRunner.class) public class UpdateProcessorServiceTest { @InjectMocks private UpdateProcessorService updateProcessorService; @Mock private ManualExclusionService manualExclusionService; @Mock private DostavchenkoClient dostavchenkoClient; @Mock private AvailabilityNotifier availabilityNotifier; @Test public void notifyAvailableIfYellowProductIsEnabledAndReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(true); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), true))); } @Test public void notifyNotAvailableIfProductIsAbsent() { final Update testProduct = new Update(1L, 0L, "Yellow"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsBlue() { final Update testProduct = new Update(1L, 10L, "Blue"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsDisabled() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(false); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsNotReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(false); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); } @Test(expected = DostavchenkoException.class) public void throwCustomExceptionIfDostavchenkoCommunicationFailed() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())) .thenThrow(new RestClientException("Something's wrong")); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); } } 


:

  • DostavchenKO ,
  • , ,

:

 @RequiredArgsConstructor @Service @Slf4j public class UpdateProcessorService { private final AvailabilityNotifier availabilityNotifier; private final DostavchenkoClient dostavchenkoClient; private final ManualExclusionService manualExclusionService; public void processUpdate(Update update) { if (update.getProductQuantity() <= 0) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if ("Blue".equals(update.getColor())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if (!manualExclusionService.isProductEnabled(update.getProductId())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } try { final boolean availableForTransportation = dostavchenkoClient.isAvailableForTransportation(update.getProductId()); availabilityNotifier.notify(new ProductAvailability(update.getProductId(), availableForTransportation)); } catch (Exception exception) { log.warn("Problems communicating with DostavchenKO", exception); throw new DostavchenkoException(); } } private ProductAvailability getNotAvailableProduct(Long productId) { return new ProductAvailability(productId, false); } } 



TDD โ€” . , :

 public void disableProduct(long productId) 

.

:

  • .
  • , , , .
  • , , , .

, - , :

  1. -, , , - , . . . , , , , . , , , . , , : , .
  2. -, . โ€” , . , , , , , , . . ProductAvailability. , . . ., , god object, , , , TDD, , . , , ยซยป โ€” : ยซ ...ยป , , TDD, .

:

 @SpringBootTest @RunWith(SpringRunner.class) public class ManualExclusionServiceTest { @Autowired private ManualExclusionService service; @Autowired private ManualExclusionRepository manualExclusionRepository; @Before public void clearDb() { manualExclusionRepository.deleteAll(); } @Test public void disableItem() { Long productId = 100L; service.disableProduct(productId); assertThat(service.isProductEnabled(productId), is(false)); } @Test public void returnEnabledIfProductWasNotDisabled() { assertThat(service.isProductEnabled(100L), is(true)); assertThat(service.isProductEnabled(200L), is(true)); } } 


 @Service @AllArgsConstructor public class ManualExclusionService { private final ManualExclusionRepository manualExclusionRepository; public boolean isProductEnabled(Long productId) { return !manualExclusionRepository.exists(productId); } public void disableProduct(long productId) { manualExclusionRepository.save(new ManualExclusion(productId)); } } 



, , , DATA .

, -, . . ProductAvailability, : productId isAvailable.

, :

  • .
  • , .
  • , .
  • , , , .
  • DATA DataCommunicationException.

, :

, , , , .

ProductAvailability , . . , , . โ€” @Document ( MongoDb) ProductAvailability.

, ProductAvailability , , , . , . . .

.

, , ProductAvailability, , , , . , ProductAvailability god object , : , , , .

 @RunWith(SpringRunner.class) @SpringBootTest public class LazyAvailabilityNotifierTest { @Autowired private LazyAvailabilityNotifier lazyAvailabilityNotifier; @MockBean @Qualifier("dataClient") private AvailabilityNotifier availabilityNotifier; @Autowired private AvailabilityRepository availabilityRepository; @Before public void clearDb() { availabilityRepository.deleteAll(); } @Test public void notifyIfFirstTime() { sendNotificationAndVerifyDataBase(new ProductAvailability(1L, false)); } @Test public void notifyIfAvailabilityChanged() { final ProductAvailability oldProductAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(oldProductAvailability); final ProductAvailability newProductAvailability = new ProductAvailability(1L, true); sendNotificationAndVerifyDataBase(newProductAvailability); } @Test public void doNotNotifyIfAvailabilityDoesNotChanged() { final ProductAvailability productAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); verify(availabilityNotifier, only()).notify(eq(productAvailability)); } @Test public void doNotSaveIfSentWithException() { doThrow(new RuntimeException()).when(availabilityNotifier).notify(anyObject()); boolean exceptionThrown = false; try { availabilityNotifier.notify(new ProductAvailability(1L, false)); } catch (RuntimeException exception) { exceptionThrown = true; } assertTrue("Exception was not thrown", exceptionThrown); assertThat(availabilityRepository.findAll(), hasSize(0)); } @Test(expected = DataCommunicationException.class) public void wrapDataException() { doThrow(new RestClientException("Something wrong")).when(availabilityNotifier).notify(anyObject()); lazyAvailabilityNotifier.notify(new ProductAvailability(1L, false)); } private void sendNotificationAndVerifyDataBase(ProductAvailability productAvailability) { lazyAvailabilityNotifier.notify(productAvailability); verify(availabilityNotifier).notify(eq(productAvailability)); assertThat(availabilityRepository.findAll(), hasSize(1)); assertThat(availabilityRepository.findAll().get(0), hasProperty("productId", is(productAvailability.getProductId()))); assertThat(availabilityRepository.findAll().get(0), hasProperty("availability", is(productAvailability.isAvailable()))); } } 


 @Component @AllArgsConstructor @Slf4j public class LazyAvailabilityNotifier implements AvailabilityNotifier { private final AvailabilityRepository availabilityRepository; private final AvailabilityNotifier availabilityNotifier; @Override public void notify(ProductAvailability productAvailability) { final AvailabilityPersistenceObject persistedProductAvailability = availabilityRepository .findByProductId(productAvailability.getProductId()); if (persistedProductAvailability == null) { notifyWith(productAvailability); availabilityRepository.save(createObjectFromProductAvailability(productAvailability)); } else if (persistedProductAvailability.isAvailability() != productAvailability.isAvailable()) { notifyWith(productAvailability); persistedProductAvailability.setAvailability(productAvailability.isAvailable()); availabilityRepository.save(persistedProductAvailability); } } private void notifyWith(ProductAvailability productAvailability) { try { availabilityNotifier.notify(productAvailability); } catch (RestClientException exception) { log.error("Couldn't notify", exception); throw new DataCommunicationException(); } } private AvailabilityPersistenceObject createObjectFromProductAvailability(ProductAvailability productAvailability) { return new AvailabilityPersistenceObject(productAvailability.getProductId(), productAvailability.isAvailable()); } } 


Kesimpulan



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

, , . , TDD , , .

, , , , , , . , , ยซยป , , , - - , .

, TDD , , . , , TDD, , - , , TDD, , .

, .

. , , , , , , TDD .

, , TDD.

, json. , , json POJO-. IDEA, , JSON.

?


. , , . . TDD . , . , , . , . . . .

, TDD , : , , , . , , .

TDD โ€” . , . , N , . , , , . N . , , , 1 god object 1 . , TDD : โ€” .

, , โ€” - . โ€” . 1,5 .

. TDD , , , . .

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


All Articles