Vaikka Codest on ensisijaisesti Ruby-liike, yksi monista projekteista, joita rakennamme, on JavaScript. Se on asiakaspuolen kirjasto, joka toimii melko haastavassa ympäristössä: sen on tuettava lähes kaikkia olemassa olevia selaimia, myös hyvin vanhoja, ja kaiken lisäksi se on vuorovaikutuksessa lukuisten ulkoisten skriptien ja palveluiden kanssa. Se on todella hauskaa.
Niputtamattomien riippuvuuksien outo tapaus
Edellä mainitut vaatimukset tuovat mukanaan koko joukon haasteita, joita ei yleensä esiinny asiakaspuolen projekti, ja yksi näistä kysymyksistä liittyy testaukseen. Meillä on tietenkin moitteeton joukko yksikkötestejä, ja ajamme niitä CI/CD-ympäristössämme hyvin suurta selain- ja käyttöjärjestelmäyhdistelmämatriisia vastaan, mutta se ei yksinään tutki kaikkea, mikä voi mennä pieleen.
Koska ekosysteemi, jossa toimimme, on yleinen arkkitehtuuri, olemme riippuvaisia siitä, että jotkin ulkoiset kirjastot ladataan omien kirjastojemme rinnalle - toisin sanoen, emme niputa niitä oman kirjastomme kanssa. koodi; emme voi, eikä asialle voi tehdä mitään. Se on mielenkiintoinen haaste, koska nämä kirjastot:
- ei ehkä edes ole siellä - jos joku sotkee kirjastomme toteuttamisen,
- saattaa olla olemassa, mutta väärinä/yhteensopimattomina versioina,
- on saatettu muuttaa jollakin muulla koodilla, joka on mukana tietyssä toteutuksessa.
Tämä osoittaa selvästi, miksi yksikkötestit eivät riitä: ne testaavat erillään todellisesta maailmasta. Sanotaan, että mallinnamme jonkin ulkoisen kirjaston julkisen API:n osan sen perusteella, mitä olemme löytäneet kirjaston dokumentaatiosta, ja suoritamme yksikkötestin sitä vastaan. Mitä se todistaa?
Saatat olla kiusaantunut sanomaan, että "se tarkoittaa, että se toimii ulkoisen kirjaston API:n kanssa", mutta olisit valitettavasti väärässä. Se tarkoittaa vain sitä, että se toimii oikein ulkoisen kirjaston julkisen API:n osajoukon kanssa, ja silloinkin vain sen version kanssa, jota olemme pilkanneet.
Entä jos kirjasto kirjaimellisesti muuttuu alta pois? Entä jos se saa outoja reaktioita, jotka saavat sen käyttämään erilaista, dokumentoimatonta koodipolkua? Voimmeko edes suojautua siltä?
Kohtuullinen suoja
Ei 100%, ei - ympäristö on siihen liian monimutkainen. Voimme kuitenkin olla kohtuullisen varmoja siitä, että kaikki toimii niin kuin pitääkin, kun meillä on joitakin yleistettyjä esimerkkejä siitä, mitä koodillemme voi tapahtua luonnossa: voimme tehdä integrointitestausta. Yksikkötesteillä varmistetaan, että koodimme toimii sisäisesti oikein, ja integrointitesteillä on varmistettava, että "keskustelemme" oikein niiden kirjastojen kanssa, joita emme voi hallita. Eikä myöskään niiden tyngän kanssa, vaan oikeiden, elävien kirjastojen kanssa.
Voisimme vain käyttää yhtä saatavilla olevista integrointitestauskehyksistä. JavaScriptrakentaa yksinkertaisen HTML-sivun, kutsua siihen muutamia kutsuja kirjastollemme ja etäyhteyksien kirjastoille ja antaa sille kunnon harjoitusta. Emme kuitenkaan halua ylikuormittaa minkään etäpalvelun päätepistettä CI/CD-ympäristömme tuottamilla kutsuilla. Se sotkisi joitain tilastoja, rikkoisi mahdollisesti joitain asioita, ja - viimeisenä mutta ei vähäisimpänä - emme olisi kovinkaan kilttejä, jos tekisimme jonkun tuotannon osaksi testejämme.
Mutta oliko integraatiotestaus näin monimutkaisen asian testaaminen edes mahdollista? Koska Ruby on ensimmäinen ja tärkein rakkautemme, turvauduimme asiantuntemukseemme ja aloimme miettiä, miten yleensä teemme integraatiotestausta etäpalveluiden kanssa Ruby-projekteissa. Saatamme käyttää jotain sellaista kuin vcr gem tallentaa tapahtumat kerran ja toistaa ne testeillemme aina tarvittaessa.
Syötä proxy
Sisäisesti vcr saavuttaa tämän välittämällä pyyntöjä. Se oli meidän a-ha! -hetkemme. Meidän piti välittää jokainen pyyntö, jonka ei pitäisi osua mihinkään "oikeassa" internetissä, joihinkin tynkävastauksiin. Sen jälkeen saapuva data välitetään ulkoiselle kirjastolle ja koodimme toimii normaalisti.
Kun teemme prototyyppejä jostain monimutkaiselta vaikuttavasta asiasta, turvaudumme usein Rubyyn aikaa säästävänä menetelmänä. Päätimme tehdä JavaScript:n testivaljaiden prototyypin Rubyllä nähdaksemme, kuinka hyvin proxy-idea toimii, ennen kuin sitoudumme rakentamaan jotain monimutkaisempaa (mahdollisesti) JavaScript:llä. Se osoittautui yllättävän yksinkertaiseksi. Itse asiassa se on niin yksinkertainen, että aiomme rakentaa sellaisen tässä artikkelissa yhdessä 🙂 .
Valot, kamera... Odota, unohdimme rekvisiitta!
Emme tietenkään ole tekemisissä "oikean asian" kanssa - edes pienen osan siitä, mitä rakennamme, selittäminen on paljon tämän blogikirjoituksen laajuuden yläpuolella. Voimme rakentaa jotakin nopeaa ja helppoa, joka korvaa kyseiset kirjastot, ja keskittyä sitten enemmän Ruby-osuuteen.
Ensinnäkin tarvitsemme jotain, joka edustaa ulkoista kirjastoa, jonka kanssa olemme tekemisissä. Tarvitsemme sen käyttäytymistä: sen pitäisi ottaa yhteyttä ulkoiseen palveluun, lähettää tapahtuma sinne tänne, ja ennen kaikkea - sitä ei pitäisi rakentaa helppoa integrointia ajatellen 🙂 .
Käytämme seuraavaa:
/* global XMLHttpRequest, Event */
const URL = 'https://poloniex.com/public/?command=returnTicker'
const METHOD = 'GET'
module.exports = {
fetch: function () {
var req = new XMLHttpRequest()
req.responseType = 'json'
req.open(METHOD, URL, true)
var doneEvent = new Event('example:fetched')
req.onreadystatechange = function (aEvt) {
if (req.readyState === 4) {
if (req.status === 200) {
this.data = req.response
} else {
this.error = true
}
window.dispatchEvent(doneEvent)
}
}.bind(this)
req.send(null)
},
error: false,
data: {}
}
Huomaat, että se kutsuu avointa sovellusliittymää joidenkin tietojen saamiseksi - tässä tapauksessa kryptovaluuttojen valuuttakursseja, koska ne ovat nykyään muotia. Tämä sovellusliittymä ei tarjoa hiekkalaatikkoa, ja se on nopeusrajoitettu, mikä tekee siitä malliesimerkin jostain, jota ei pitäisi oikeastaan käyttää testeissä.
Saatat huomata, että tämä on itse asiassa NPM-yhteensopiva moduuli, kun taas olen vihjannut, että tavallisesti käsittelemämme skripti ei ole saatavilla NPM:ssä helppoa niputtamista varten. Tähän esittelyyn riittää, että se käyttäytyy tietyllä tavalla, ja haluaisin mieluummin helpottaa selittämistä liiallisen yksinkertaistamisen kustannuksella.
Näyttelijöiden kutsuminen
Nyt tarvitsemme myös jotain kirjastomme tilalle. Jälleen kerran pidämme vaatimukset yksinkertaisina: sen on kutsuttava "ulkoista" kirjastoamme ja tehtävä jotain tulosteella. Jotta "testattavuus" pysyisi yksinkertaisena, laitamme sen myös tekemään kahdenlaista kirjausta: sekä konsoliin, jota on hieman vaikeampi lukea spekseissä, että globaalisti käytettävissä olevaan joukkoon.
window.remote = require('remote-calling-example')
window.failedMiserably = true
window.logs = []
function log (message) {
window.logs.push(message)
console.log(message)
}
window.addEventListener('example:fetched', function () {
if (window.remote.error) {
log('[EXAMPLE] Remote fetch failed')
window.failedMiserably = true
} else {
log('[EXAMPLE] Remote fetch successful')
log([EXAMPLE] BTC to ETH: ${window.remote.data.BTC_ETH.last})
}
})
window.remote.fetch()
Pidän myös käyttäytymisen tarkoituksella hämmästyttävän yksinkertaisena. Nykyisellään on vain kaksi oikeastaan kiinnostavaa koodipolkua, joita varten on olemassa speksejä, joten emme joudu speksejä vyöryn alle, kun rakennamme sitä eteenpäin.
Kaikki vain napsahtaa yhteen
Laitetaan yksinkertainen HTML-sivu:
<code> <!DOCTYPE html>
<html>
<head>
<title>Esimerkkisivu</title>
<script type="text/javascript" src="./index.js"></script>
</head>
<body></body>
</html>
Tätä demoa varten niputamme HTML:n ja JavaScript:n yhteen seuraavalla tavalla Paketti, hyvin yksinkertainen verkkosovellus niputtaja. Pidän Parcelista paljon tällaisissa tilanteissa, kun olen laatimassa nopeaa esimerkkiä tai hakkaamassa ideaa, joka on syntynyt nenäliinan pohjalta. Kun teet jotain niin yksinkertaista, että Webpackin konfigurointi veisi enemmän aikaa kuin haluamasi koodin kirjoittaminen, se on paras vaihtoehto.
Se on myös tarpeeksi huomaamaton, että kun haluan vaihtaa johonkin, joka on hieman enemmän testattu, minun ei tarvitse tehdä lähes mitään takapakkia Parcelista, mitä ei voi sanoa Webpackista. Varoitus kuitenkin - Parcel on kovassa kehitysvaiheessa, ja ongelmia voi esiintyä ja tulee esiintymään; minulla on ollut ongelma, jossa transpiloitu JavaScript-tulostus oli virheellinen vanhemmassa Node.js. Lopputulos: älä ota sitä vielä osaksi tuotantoputkea, mutta kokeile sitä kuitenkin.
Integraation voiman valjastaminen
Nyt voimme rakentaa testivalikoimamme.
Itse spesifikaatiokehyksessä olemme käyttäneet rspec. Kehitysympäristöissä testaamme todellisella, ei-headless Chromella - sen ajaminen ja valvominen on annettu tehtäväksi watir (ja sen luotettava apuri watir-rspec). Olemme kutsuneet valtakirjan välittäjäksi Puffing Billy ja teline juhliin. Lopuksi haluamme suorittaa JavaScript-rakentamisemme uudelleen joka kerta, kun suoritamme speksejä, ja se onnistuu seuraavalla toiminnolla kokaiini.
Siinä on koko joukko liikkuvia osia, joten speksejä käyttävä apuohjelmamme on hieman... mukana jopa tässä yksinkertaisessa esimerkissä. Katsotaanpa sitä ja puretaan se osiin.
Dir['./spec/support/*/.rb'].each { |f| require f }
TEST_LOGGER = Logger.new(STDOUT)
RSpec.configure do |config|
config.before(:suite) { Cocaine::CommandLine.new('npm', 'run build', logger: TEST_LOGGER).run }
config.include Watir::RSpec::Helper
config.include Watir::RSpec::Matchers
config.include ProxySupport
config.order = :random
BrowserSupport.configure(config)
end
Billy.configure do |c|
c.cache = false
c.cacherequestheaders = false
c.persistcache = false
c.recordstubrequests = true
c.logger = Logger.new(File.expandpath('../log/billy.log', FILE))
end
Ennen koko sviittiä ajamme mukautetun rakennuskomentomme kokaiinin kautta. Tuo TEST_LOGGER-vakio saattaa olla hieman liikaa, mutta emme ole kovin huolissamme objektien määrästä. Ajamme tietysti speksejä satunnaisessa järjestyksessä, ja meidän on otettava mukaan kaikki watir-rspecin herkut. Meidän on myös asetettava Billy niin, että se ei tallenna tietoja välimuistiin, mutta kirjaa ne laajasti tiedostoon spec/log/billy.log
. Jos et tiedä, tuleeko pyyntö oikeasti pysäytetyksi vai osuuko se elävälle palvelimelle (hups!), tämä loki on puhdasta kultaa.
Olen varma, että tarkat silmäsi ovat jo huomanneet ProxySupportin ja BrowserSupportin. Saatat luulla, että mukautetut herkut ovat siellä... ja olisit aivan oikeassa! Katsotaanpa ensin, mitä BrowserSupport tekee.
Selain, valvottu
Ensiksi esitellään TempBrowser
:
luokka TempBrowser
def get
@browser ||= Watir::Browser.new(web_driver)
end
def kill
@browser.close if @browser
@browser = nil
end
private
def web_driver
Selenium::WebDriver.for(:chrome, options: options)
end
def options
Selenium::WebDriver::Chrome::Options.new.tap do |options|
options.addargument '--auto-open-devtools-for-tabs' (--auto-open-devtools-for-tabs)
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
end
end
end
Kutsupuusta taaksepäin tarkastelemalla näemme, että määrittelemme Seleniumin selainasetukset Chromelle. Yksi asetuksista, jotka annamme siihen, on tärkeä asetuksemme kannalta: se ohjaa Chrome-instanssia välittämään kaiken Puffing Billy -instanssimme kautta. Toinen vaihtoehto on vain kiva olla - jokainen käyttämämme instanssi, joka ei ole päätön tarkastustyökalut avautuvat automaattisesti. Näin säästämme lukemattomia määriä Cmd+Alt+I:tä päivässä 😉.
Kun olemme asettaneet selaimen näillä asetuksilla, annamme sen Watirille, ja se on siinä. . tappaa
metodi on pieni lisäke, jonka avulla voimme toistuvasti pysäyttää ja käynnistää ajurin uudelleen tarvittaessa ilman, että TempBrowser-instanssi heitetään pois.
Nyt voimme antaa rspec-esimerkillemme pari supervoimaa. Ensinnäkin, saamme näppärän selain
apumenetelmä, jonka ympärillä speksimme pääosin pyörivät. Voimme myös käyttää kätevää metodia selaimen uudelleenkäynnistämiseksi tietyn esimerkin osalta, jos teemme jotain erittäin herkkää. Tietenkin haluamme myös lopettaa selaimen testisarjan jälkeen, koska emme missään tapauksessa halua Chrome-olioita viipymään - RAM-muistin vuoksi.
moduuli BrowserSupport
def self.browser
@browser ||= TempBrowser.new
end
def self.configure(config)
config.around(:each) do |example|
BrowserSupport.browser.kill if example.metadata[:clean]
@browser = BrowserSupport.browser.get
@browser.cookies.clear
@browser.driver.manage.timeouts.implicit_wait = 30
example.run
end
config.after(:suite) do
BrowserSupport.browser.kill
end
end
end
Välityspalvelimen kytkeminen
Meillä on selain ja spesifikaatioapurit, ja olemme valmiita aloittamaan pyyntöjen välittämisen välityspalvelimellemme. Mutta odota, emme ole asettaneet sitä vielä! Voisimme tehdä toistuvia kutsuja Billyyn jokaista esimerkkiä varten, mutta on parempi hankkia itsellemme pari apumetodia ja säästää pari tuhatta näppäinpainallusta. Se on se, mitä ProxySupport
tekee.
Testiasetuksessamme käyttämämme järjestelmä on hieman monimutkaisempi, mutta tässä on yleiskuvaus:
frozenstringliteral: true
vaadi 'json'
moduuli ProxySupport
HEADERS = {
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'X-Requested-With, X-Prototype-Version, Content-Type',
'Access-Control-Allow-Origin' => '*'.
}.freeze
def stubjson(url, file)
Billy.proxy.stub(url).andreturn({ {
body: open(file).read,
code: 200,
headers: dup
})
end
def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
body: '',
code: status,
headers: dup
})
end
def stubpage(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'text/html',
code: 200
)
end
def stubjs(url, file)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'application/javascript',
code: 200
)
end
end
Voimme tukkia:
- HTML-sivupyynnöt - tärkeimmälle "leikkipaikka"-sivulle,
- JS-pyynnöt - palvelemaan niputettua kirjastoamme,
- JSON-pyynnöt - pyyntöjen tynkkäys etä-API:lle,
- ja vain "mikä tahansa" -pyyntö, jossa meitä kiinnostaa vain tietyn, ei-200 HTTP-vastauksen palauttaminen.
Tämä riittää hyvin yksinkertaiseen esimerkkiin. Esimerkeistä puheen ollen - meidän pitäisi luoda pari!
Hyvän puolen testaaminen
Meidän on ensin luotava pari "reittiä" välityspalvelimellemme:
let(:pageurl) { 'http://myfancypage.local/index.html' }
let(:jsurl) { 'http://myfancypage.local/dist/remote-caller-example.js' }
let(:pagepath) { './dist/index.html' }
let(:jspath) { './dist/remote-caller-example.js' }
before do
stubpage pageurl, pagepath
stubjs jsurl, jspath
end
Kannattaa huomata, että rspecin näkökulmasta suhteelliset polut viittaavat projektin päähakemistoon, joten lataamme HTML:n ja JS:n suoraan tiedostosta dist
hakemisto - kuten Parcel on rakentanut. Voit jo nähdä, miten nämä stub_*
avustajat ovat käteviä.
Kannattaa myös huomata, että sijoitamme "väärennetyn" verkkosivustomme osoitteeseen .local
TLD. Tällä tavoin kaikki karkaavat pyynnöt eivät pääse pakenemaan paikallisesta ympäristöstämme, jos jokin menee pieleen. Yleisenä käytäntönä suosittelen, että ainakaan "oikeita" verkkotunnuksia ei käytetä tyngissä, ellei se ole ehdottoman välttämätöntä.
Toinen huomautus, joka meidän on tehtävä tässä yhteydessä, koskee sitä, että emme saa toistaa itseämme. Kun välityspalvelinreititys monimutkaistuu ja sisältää paljon enemmän polkuja ja URL-osoitteita, on todellista hyötyä siitä, että tämä asetus puretaan jaettuun kontekstiin ja yksinkertaisesti sisällytetään tarvittaessa.
Nyt voimme määritellä, miltä "hyvän" polun pitäisi näyttää:
asiayhteys 'oikealla vastauksella' do
before do
stubjson %r{http://poloniex.com/public(.*)}, './spec/fixtures/remote.json'.
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 2') }
end
it 'loggaa oikeat tiedot' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] Remote fetch successful', '[EXAMPLE] BTC to ETH: 0.03619999'])
)
end
end
Eikö se olekin aika yksinkertaista? Tässä on vielä hieman lisää asetuksia - me tynkätään JSON-vastaus etä-API:ltä kiinnikkeellä, siirrytään pää-URL-osoitteeseen ja sitten... odotetaan.
Pisin odotus
Odotukset ovat tapa kiertää Watirin kanssa kohtaamamme rajoitus - emme voi luotettavasti odottaa esimerkiksi JavaScript-tapahtumia, joten meidän on huijattava hieman ja "odotettava", kunnes skriptit ovat siirtäneet jonkin objektin, jota voimme käyttää, meitä kiinnostavaan tilaan. Huonona puolena on se, että jos kyseistä tilaa ei koskaan tule (esimerkiksi bugin takia), meidän on odotettava, että watir waiterin aika loppuu. Tämä kasvattaa spekseihin kuluvaa aikaa hieman. Speksejä ei kuitenkaan silti saada luotettavasti epäonnistumaan.
Kun sivu on "vakiintunut" meitä kiinnostavaan tilaan, voimme suorittaa vielä joitakin JavaScript-ohjelmia sivun yhteydessä. Tässä kutsumme julkiseen arrayyn kirjoitetut lokit ja tarkistamme, ovatko ne sitä, mitä odotimme.
Sivuhuomautuksena - tässä kohtaa etäkäyttökyselyn tynkkäys todella loistaa. Konsoliin kirjautuva vastaus on riippuvainen etä-API:n palauttamasta vaihtokurssista, joten emme voisi luotettavasti testata lokin sisältöä, jos se muuttuisi jatkuvasti. On toki olemassa keinoja kiertää se, mutta ne eivät ole kovin tyylikkäitä.
Huonon haaran testaaminen
Vielä yksi asia testattavaksi: "failure"-haara.
context 'epäonnistuneella vastauksella' do
before do
stubstatus %r{http://poloniex.com/public(.*)}, 404
gooto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') }
end
it 'lokien epäonnistuminen' do
expect(browser.execute_script('return window.logs')).to(
eq(['[EXAMPLE] Etäisyyshaku epäonnistui']))
)
end
end
Se on hyvin samankaltainen kuin edellä, sillä erona on, että tynkän vastauksen palautetaan HTTP-tilakoodi 404 ja odotetaan erilaista lokia.
Käynnistetään speksit nyt.
%-nippu exec rspec
Satunnaistettu siemenellä 63792
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : Komento :: npm run build
Etäkutsu
oikealla vastauksella
kirjaa oikeat tiedot
epäonnistuneella vastauksella
kirjaa epäonnistumisen
Valmis 23,56 sekunnissa (tiedostojen lataaminen kesti 0,86547 sekuntia).
2 esimerkkiä, 0 epäonnistumista
Woohoo!
Päätelmä
Olemme keskustelleet lyhyesti siitä, miten JavaScript:tä voidaan testata Rubyn kanssa. Vaikka alun perin sitä pidettiin enemmänkin välikappaleena, olemme nyt melko tyytyväisiä pieneen prototyyppiimme. Harkitsemme tietysti edelleen puhdasta JavaScript-ratkaisua, mutta sillä välin meillä on yksinkertainen ja käytännöllinen tapa toistaa ja testata joitakin hyvin monimutkaisia tilanteita, joita olemme kohdanneet luonnossa.
Jos harkitset vastaavanlaisen laitteen rakentamista itse, on huomattava, että sillä on omat rajoituksensa. Jos esimerkiksi testaamastasi ohjelmasta tulee todella AJAX-painotteinen, Puffing Billylla kestää kauan vastata. Myös jos sinun täytyy käyttää SSL-lähteitä, tarvitaan enemmän näpertelyä - tutustu watirin dokumentaatioon, jos se on vaatimuksesi. Jatkamme varmasti tutkimista ja etsimme parhaita tapoja käsitellä ainutlaatuista käyttötapaustamme - ja kerromme varmasti myös sinulle, mitä saimme selville.