de Oleksii Fedorov

O modalitate fără stres de a testa apelurile frustrante ale metodelor statice în Kotlin

Permiteți-mi să ghicesc sălbatic … Ați întâlnit un cod în Kotlin care folosește o bibliotecă de la terți. API-ul oferit de bibliotecă este una sau câteva metode statice. Și doriți să testați un anumit cod folosind aceste metode statice. Este dureros.

Nu sunteți sigur cum să abordați această problemă.

Poate vă întrebați: „Când vor înceta autorii unor biblioteci terță parte să folosească metode statice?”

Oricum, cine sunt eu pentru a vă spune cum să testați apelurile de metode statice în Kotlin?

Sunt un fanatic al testării și al evanghelistului dezvoltării bazate pe testare în ultimii cinci ani – mă sună Bursier TDD pentru un motiv. Lucrez cu Kotlin în producție de aproximativ doi ani în momentul în care am scris acest lucru.

Înainte!

Așa mă simt când văd API-uri atât de îngrozitoare:

O modalitate fara stres de a testa apelurile frustrante ale
(sursa: pexels.com)

Permiteți-mi să vă arăt ce vreau să spun cu un exemplu dur cu care am avut de-a face recent. Biblioteca era o newrelic client. Pentru a-l folosi, a trebuit să apelez la o metodă statică pentru unele clase. Dacă este simplificat, arată cam așa:

NewRelicClient.addAttributesToCurrentRequest(“orderId”, order.id)

Trebuia să schimb exact ce trimitem și a trebuit să adaug mai multe atribute. Din moment ce am vrut să am încredere că schimbarea mea nu rupe nimic și face exact ceea ce vreau, am avut nevoie să scriu un test. Nu a existat încă niciun test pentru acest cod.

Dacă tot citești, presupun că te afli în aceeași situație. Sau ai fost în trecut.

Sunt de acord că este o situație dureroasă.

Cum ar trebui să bat joc de aceste apeluri în test?

Știu, este frustrant faptul că majoritatea bibliotecilor de batjocură nu sunt capabile să batjocorească apelurile de metode statice. Și chiar și cei care funcționează în Java nu funcționează întotdeauna în Kotlin.

Există biblioteci care ar putea face asta, cum ar fi powermock, de exemplu. Dar tu stii ce? Poate că utilizați deja mockito sau vreo altă bibliotecă. Adăugarea unui alt instrument de batjocură la proiect va face lucrurile mai confuze și frustrante.

Știu cât de enervant este să ai mai multe instrumente pentru același job în aceeași bază de cod. Asta provoacă o mare confuzie pentru toată lumea.

Ei bine, acea problemă a fost deja rezolvată acum aproximativ două decenii!

Interesat? Vino la plimbare.

Refactorizarea către Obiectul Umil

Să aruncăm o privire asupra codului cu care lucrăm aici:

class FulfilOrderService {

    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        NewRelicClient.addAttributesToCurrentRequest(
                "orderId", order.id)
        NewRelicClient.addAttributesToCurrentRequest(
                "orderAmount", order.amount.toString())
                
    }
    
}

Face diverse lucruri cu ordinul de a-l îndeplini și apoi atribuie câteva atribute cererii curente pentru newrelic.

Primul lucru pe care îl vom face împreună aici este extragerea metodei addAttributesToRequest. De asemenea, dorim să-l parametrizăm cu key și value argumente. Puteți face acest lucru manual sau, dacă aveți norocul de a utiliza IntelliJ IDEA, puteți face automat refactorizarea.

Iată cum:

  1. Selectați ”orderId” și extrageți o variabilă locală. Numeste-l key.
  2. Selectați order.id și extrageți o variabilă locală. Numeste-l value.
  3. Selectați NewRelicClient.addAttributesToCurrentRequest(key, value) și extrageți o metodă. Numeste-l addAttributesToRequest.
  4. IntelliJ va evidenția cel de-al doilea apel către NewRelicClient ca duplicat și vă spun că îl puteți înlocui cu apelul către noua metodă privată. IntelliJ vă va întreba dacă doriți să faceți asta. Fă-o.
  5. Variabile în linie key și value.
  6. În cele din urmă, faceți metoda protected in loc de private. Vă voi arăta într-un pic de ce trebuie protejată metoda.
  7. Veți observa că IntelliJ evidențiază protected cu un avertisment. Asta pentru că toate clasele din Kotlin sunt final în mod implicit. Deoarece clasele finale nu pot fi extinse, protected este inutil. Una dintre soluțiile oferite de IntelliJ este de a face clasa open. Fă-o. Metoda addAttributesToRequest ar trebui să devină și ele deschise.

Iată ce ar trebui să obțineți în final:

open class FulfilOrderService {

    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        addAttributesToRequest("orderId", order.id)
        addAttributesToRequest("orderAmount",
                               order.amount.toString())
                               
    }
    
    protected open fun addAttributesToRequest(key: String,
                                              value: String) {
                                              
        NewRelicClient.addAttributesToCurrentRequest(key, value)
        
    }
    
}

Observați cum toate aceste refactorizări au fost complet automate și, prin urmare, sigure de executat. Nu avem nevoie de teste pentru a le face. Având această metodă protejată ne va oferi posibilitatea de a scrie un test:

private val attributesAdded = mutableListOf<Pair<String, String>>()

private val subject = FulfilOrderService()

@Test
fun `adds order id to the current request within newrelic`() {

    val order = Order(id = "some-id", amount = 142)
    
    subject.fulfil(order)
    
    val expectedAttributes = listOf(
            Pair("orderId", "some-id"),
            Pair("orderAmount", "142"))
    assertEquals(expectedAttributes, attributesAdded)
    
}

Apropo de teste și refactorizare …

Vrei să înveți cum să scrii un test de acceptare în Kotlin? Poate, cum să folosiți puterea IntelliJ IDEA în avantajul dvs.?

Poate doriți să aflați cum să creați aplicații bine în Kotlin? – fie că este vorba de aplicații din linia de comandă, web sau Android?

Există această carte e-tutorial supremă pe care am scris-o ACCIDENTAL despre începutul cu Kotlin. 350 de pagini de tutorial practic pe care le puteți urmări.

Te vei simți de parcă aș sta împreună cu tine și ne vom bucura de timpul nostru, construind tot timpul o aplicație completă din linia de comandă.

Interesat?

Descărcați fișierul tutorial final aici. Apropo, este gratuit și va fi întotdeauna!

Revenind la testul nostru.

Totul arată corect, dar nu funcționează, deoarece nimeni nu adaugă elemente în listă attributesAdded. Deoarece avem acea mică metodă protejată, putem să „intrăm în ea”:

private val subject: FulfilOrderService = object :
                                          FulfilOrderService() {
                                          
    override fun addAttributesToRequest(key: String,
                                        value: String) {
                                        
        attributesAdded.add(Pair(key, value))
        
    }
    
}

Dacă rulați testul, acesta trece. Puteți modifica valorile din codul de testare sau de producție pentru a vedea eșecul și a vă asigura că într-adevăr testează ceea ce credeți că face.

Să vedem întregul cod de testare:

import org.junit.Assert.*
import org.junit.Test

@Suppress("FunctionName")
class FulfilOrderServiceTest {

    private val attributesAdded = 
            mutableListOf<Pair<String, String>>()
            
    private val subject: FulfilOrderService = object :
                                      FulfilOrderService() {
                                      
        override fun addAttributesToRequest(key: String,
                                            value: String) {
                                            
            attributesAdded.add(Pair(key, value))
            
        }
        
    }
    
    @Test
    fun `adds order id to the current request within newrelic`() {
    
        val order = Order(id = "some-id", amount = 142)
        
        subject.fulfil(order)
        
        val expectedAttributes = listOf(
                Pair("orderId", "some-id"),
                Pair("orderAmount", "142"))
        assertEquals(expectedAttributes, attributesAdded)
        
    }
    
}

Deci, ce tocmai s-a întâmplat aici?

Vezi, am făcut o versiune ușor diferită a FulfilOrderService clasa – una testabilă. Singura slăbiciune a acestei metode de testare este că, dacă cineva se descurcă addAttributesToRequest funcție, nu se va sparge niciun test.

Pe de altă parte, această funcție nu va trebui să conțină niciodată mai mult de o linie de cod simplu și probabil că nu se va schimba atât de des. Acest lucru se va întâmpla numai în cazul în care autorii bibliotecii terță parte pe care o folosim vor introduce o schimbare de rupere în acea metodă unică.

Este puțin probabil. Se va întâmpla probabil la fiecare câțiva ani.

Și știi ce?

Chiar dacă îl testați într-un fel mai mult „black-box’ey” decât ceea ce ofer aici, atunci când o astfel de schimbare de rupere vine în jurul blocului, va trebui totuși să vizitați din nou toate uzanțele și să le remediați. Probabil că va trebui să aruncați sau să rescrieți și toate testele aferente.

Oh, și în cazul unei astfel de modificări de rupere, aș recomanda testarea manuală cel puțin o dată pentru a vedea dacă ați înțeles corect noul API și interacționează cu sistemul terț într-un mod în care credeți că ar trebui.

Având în vedere toate aceste informații, cred că ar trebui să fie bine să lăsați acea linie netestată.

Dar dacă o astfel de schimbare are loc în jurul blocului, trebuie să vânați toate locurile în care apelăm NewRelicClient?

Răspuns scurt – da.

Răspuns lung: în designul actual – da. Dar ai crezut că am terminat aici?

Nu.

Designul este teribil așa cum este acum. Să remediem asta prin extragerea obiectului umil. Odată ce vom face acest lucru, va exista un singur loc într-o bază de cod întreg care va necesita schimbare – acel obiect umil.

Din păcate, IntelliJ nu acceptă Move method sau Extract method object refactorizarea pentru Kotlin destul de încă, așa că va trebui să o efectuăm manual.

Dar tu stii ce? – Este OK, deoarece avem deja teste conexe care ne susțin!

Pentru a face Extract method object refactoring, va trebui să înlocuim implementarea din interiorul metodei cu crearea obiectului și să apelăm imediat la metoda acelui obiect cu aceleași argumente ca și metoda refactored:

protected open fun addAttributesToRequest(key: String,
                                          value: String) {
                                          
//   NewRelicClient.addAttributesToCurrentRequest(key, value)
    NewRelicHumbleObject().addAttributesToRequest(key, value)
    
}

Apoi va trebui să creăm această clasă și să creăm metoda pe ea. În cele din urmă, vom pune conținutul metodei refactorizate, cea pe care am comentat-o, la metoda proaspăt creată; nu uitați să eliminați comentariul deoarece nu mai avem nevoie de el:

class NewRelicHumbleObject {
    
    fun addAttributesToRequest(key: String, value: String) {
        
        NewRelicClient.addAttributesToCurrentRequest(key, value)
        
    }
    
}

Am terminat cu acest pas de refactorizare și ar trebui să ne efectuăm testele acum. Toți ar trebui să treacă dacă nu am greși – și așa fac!

Următorul pas în această refactorizare este mutarea creației obiectului umil în câmp. Aici putem efectua o refactorizare automată pentru a extrage câmpul din expresie NewRelicHumbleObject(). Asta ar trebui să obțineți după refactorizare:

private val newRelicHumbleObject = NewRelicHumbleObject()

protected open fun addAttributesToRequest(key: String,
                                          value: String) {
                                          
    newRelicHumbleObject.addAttributesToRequest(key, value)
    
}

Acum, pentru că avem acea valoare în câmp, o putem muta la constructor. Există și o refacere automată pentru asta! Se numeste Move to constructor. Ar trebui să obțineți următorul rezultat:

open class FulfilOrderService(
        private val newRelicHumbleObject: NewRelicHumbleObject =
                                          NewRelicHumbleObject()) {
                                          
    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        addAttributesToRequest("orderId", order.id)
        addAttributesToRequest("orderAmount",
                               order.amount.toString())
                               
    }
    
    protected open fun addAttributesToRequest(key: String,
                                              value: String) {
                                              
        newRelicHumbleObject.addAttributesToRequest(key, value)
        
    }
    
}

Acest lucru va face foarte simplu să injectați dependența din test. Și observați, este un obiect obișnuit cu o metodă nestatică.

Știi ce înseamnă asta?

Da! Puteți folosi instrumentul dvs. de batjocură preferat pentru a bate joc de asta. Hai să facem asta acum. Voi folosi mockito pentru acest exemplu.

În primul rând, va trebui să creăm simularea în testul nostru:

private val newRelicHumbleObject =
        Mockito.mock(NewRelicHumbleObject::class.java)

Pentru a ne putea bate joc de umilul nostru obiect, va trebui să-i facem clasa open și metoda addAttributesToRequest deschide și tu:

open class NewRelicHumbleObject {

    open fun addAttributesToRequest(key: String, value: String) {
        // ...
        
    }
    
}

Apoi, va trebui să oferim această batjocură ca argument FulfilOrderServiceconstructorul:

private val subject = FulfilOrderService(newRelicHumbleObject)

În cele din urmă, vrem să înlocuim afirmația noastră cu mockitoverificarea:

Mockito.verify(newRelicHumbleObject)
        .addAttributesToRequest("orderId", "some-id")
Mockito.verify(newRelicHumbleObject)
        .addAttributesToRequest("orderAmount", "142")
Mockito.verifyNoMoreInteractions(newRelicHumbleObject)

Aici verificăm dacă metoda obiectului nostru umil addAttributesToRequest a fost chemat cu argumente adecvate de două ori și cu nimic altceva. Și nu avem nevoie attributesAdded câmp mai, așa că hai să scăpăm de asta.

Iată ce ar trebui să obțineți acum:

class FulfilOrderServiceTest {

    private val newRelicHumbleObject =
            Mockito.mock(NewRelicHumbleObject::class.java)
            
    private val subject = FulfilOrderService(newRelicHumbleObject)
    
    @Test
    fun `adds order id to the current request within newrelic`() {
    
        val order = Order(id = "some-id", amount = 142)
        
        subject.fulfil(order)
        
        Mockito.verify(newRelicHumbleObject)
                .addAttributesToRequest("orderId", "some-id")
        Mockito.verify(newRelicHumbleObject)
                .addAttributesToRequest("orderAmount", "142")
        Mockito.verifyNoMoreInteractions(newRelicHumbleObject)
        
    }
    
}

Acum, că nu mai înlocuim acea metodă protejată, o putem pune în linie. Apropo, clasa nu trebuie să fie open mai mult. Al nostru FulfilOrderService clasa este acum gata să accepte modificările pe care am vrut să le facem, deoarece este testabilă acum (cel puțin în ceea ce privește newrelic atribute de cerere):

class FulfilOrderService(
        private val newRelicHumbleObject: NewRelicHumbleObject = 
                                          NewRelicHumbleObject()) {
                                          
    fun fulfil(order: Order) {
    
        // .. do various things ..
        
        newRelicHumbleObject.addAttributesToRequest(
                "orderId", order.id)
        newRelicHumbleObject.addAttributesToRequest(
                "orderAmount", order.amount.toString())
                
    }
    
}

Să rulăm din nou toate testele, doar pentru o măsură bună! – trec cu toții.

Super, cred că am terminat aici.

Împărtășiți ce credeți despre Humble Object!

Mulțumesc că ai citit!

M-ar face fericit dacă ați împărtăși ceea ce credeți despre o astfel de refactorizare în comentarii. Știți o modalitate mai simplă de refactorizare? – acțiune!

De asemenea, dacă îți place ceea ce vezi, ia în considerare să îmi dai o palmă pe Medium și să împărtășești articolul pe social media.

Dacă sunteți interesat să învățați Kotlin și vă place stilul meu de scris, luați-mi tutorialul final pentru a începe cu Kotlin.

Cum „@Deprecated” Kotlin ameliorează durerea de refacere colosală?
Îți voi spune o poveste reală despre cum ne-am economisit de multe ori. Puterea refactorizării @Deprecated a lui Kotlin …
hackernoon.com

Cum devorează Kotlin Calamity aplicațiile dvs. Java ca Lightning?
Aud ce spui. Există acel buzz din jurul Android care adoptă activ Kotlin ca programare principală …
hackernoon.com

Refactorizarea schimbării în paralel
Schimbarea paralelă este tehnica de refactorizare care permite implementarea modificărilor incompatibile înapoi la un API într-un seif …
medium.com