window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', versjon: 2, } ;(function () { var w = vindu if (w.LeadBooster) { console.warn('LeadBooster finnes allerede') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() TESTING AV JAVASCRIPT... MED RUBY?! - The Codest
The Codest
  • Om oss
  • Tjenester
    • Programvareutvikling
      • Frontend-utvikling
      • Backend-utvikling
    • Staff Augmentation
      • Frontend-utviklere
      • Backend-utviklere
      • Dataingeniører
      • Ingeniører i skyen
      • QA-ingeniører
      • Annet
    • Det rådgivende
      • Revisjon og rådgivning
  • Industrier
    • Fintech og bankvirksomhet
    • E-commerce
    • Adtech
    • Helseteknologi
    • Produksjon
    • Logistikk
    • Bilindustrien
    • IOT
  • Verdi for
    • ADMINISTRERENDE DIREKTØR
    • CTO
    • Leveransesjef
  • Vårt team
  • Casestudier
  • Vet hvordan
    • Blogg
    • Møter
    • Webinarer
    • Ressurser
Karriere Ta kontakt med oss
  • Om oss
  • Tjenester
    • Programvareutvikling
      • Frontend-utvikling
      • Backend-utvikling
    • Staff Augmentation
      • Frontend-utviklere
      • Backend-utviklere
      • Dataingeniører
      • Ingeniører i skyen
      • QA-ingeniører
      • Annet
    • Det rådgivende
      • Revisjon og rådgivning
  • Verdi for
    • ADMINISTRERENDE DIREKTØR
    • CTO
    • Leveransesjef
  • Vårt team
  • Casestudier
  • Vet hvordan
    • Blogg
    • Møter
    • Webinarer
    • Ressurser
Karriere Ta kontakt med oss
Pil tilbake GÅ TILBAKE
2019-02-04
Programvareutvikling

TESTING AV JAVASCRIPT ... MED RUBY?!

Pawel Wal

Selv om Codest først og fremst er en Ruby-butikk, er et av de mange prosjektene vi bygger i JavaScript. Det er et klientsidebibliotek som kjører i et ganske utfordrende miljø: Det må støtte så å si alle nettlesere som finnes, inkludert veldig gamle, og i tillegg samhandler det med en mengde eksterne skript og tjenester. Det er veldig gøy.

Det merkelige tilfellet med ikke-bundne avhengigheter

Med de ovennevnte kravene følger en rekke utfordringer som vanligvis ikke er til stede på klientsiden prosjektog en av disse problemene har med testing å gjøre. Vi har selvfølgelig et upåklagelig sett med enhetstester, og vi kjører dem mot en veldig stor matrise av nettleser-/operativsystemkombinasjoner i CI/CD-miljøet vårt, men det alene utforsker ikke alt som kan gå galt.

På grunn av den overordnede arkitekturen i økosystemet vi kjører i, er vi avhengige av at noen eksterne biblioteker lastes inn sammen med våre - det vil si at vi ikke pakker dem sammen med vår kode; det kan vi ikke, og det er ikke noe å gjøre med det. Det er en interessant utfordring, fordi disse bibliotekene:

  • kanskje ikke engang er der - hvis noen roter til implementeringen av biblioteket vårt,
  • kan være der, men i feil/inkompatible versjoner,
  • kan ha blitt modifisert av annen kode som er med på ferden i en bestemt implementering.

Dette viser tydelig hvorfor enhetstester ikke er nok: de tester isolert fra den virkelige verden. La oss si at vi modellerer en del av et eksternt biblioteks offentlige API, basert på det vi har funnet ut i dokumentasjonen, og kjører en enhetstest mot det. Hva beviser det?

Du kan bli fristet til å si "det betyr at det fungerer med det eksterne bibliotekets API", men det er dessverre feil. Det betyr bare at den samhandler korrekt med en delmengde av det eksterne bibliotekets offentlige API, og selv da bare med den versjonen vi har modellert.

Hva om biblioteket bokstavelig talt endrer seg under oss? Hva om det - ute i naturen - får noen merkelige responser som får det til å slå inn på en annen, udokumentert kodebane? Kan vi i det hele tatt beskytte oss mot det?

Rimelig beskyttelse

Ikke 100%, nei - miljøet er for komplekst til det. Men vi kan være rimelig sikre på at alt fungerer som det skal med noen generaliserte eksempler på hva som kan skje med koden vår ute i naturen: Vi kan gjøre integrasjonstesting. Enhetstestene sikrer at koden vår kjører som den skal internt, og integrasjonstestene må sørge for at vi "snakker" ordentlig med bibliotekene vi ikke kan kontrollere. Og ikke med stubber av dem heller - faktiske, levende biblioteker.

Vi kan bare bruke et av de tilgjengelige rammeverkene for integrasjonstester for JavaScriptVi kan bygge en enkel HTML-side, legge inn noen kall til biblioteket vårt og de eksterne bibliotekene på den, og gi den en god treningsøkt. Vi ønsker imidlertid ikke å oversvømme noen av de eksterne tjenestenes endepunkter med anrop generert av CI/CD-miljøene våre. Det ville rote med noen statistikker, muligens ødelegge noen ting, og - sist, men ikke minst - det ville ikke være særlig hyggelig å gjøre noens produksjon til en del av testene våre.

Men var det i det hele tatt mulig å integrasjonsteste noe så komplekst? Siden Ruby er vår første og største kjærlighet, trakk vi på ekspertisen vår og begynte å tenke på hvordan vi vanligvis gjør integrasjonstesting med eksterne tjenester i Ruby-prosjekter. Vi bruker kanskje noe sånt som video gem for å registrere det som skjer én gang, og deretter fortsette å spille det av til testene våre når det er nødvendig.

Angi proxy

Internt oppnår vcr dette ved å proxy-forespørsler. Det var vår aha-opplevelse. Vi trengte å sende alle forespørsler som ikke skulle treffe noe på det "ekte" internett, til noen stubbed-svar. Deretter blir de innkommende dataene sendt til det eksterne biblioteket, og koden vår kjører som vanlig.

Når vi skal lage prototyper av noe som virker komplisert, faller vi ofte tilbake på Ruby som en tidsbesparende metode. Vi bestemte oss for å lage en prototyp for JavaScript i Ruby for å se hvor godt proxy-ideen vil fungere før vi forpliktet oss til å bygge noe mer komplisert i (muligens) JavaScript. Det viste seg å være overraskende enkelt. Faktisk så enkelt at vi kommer til å bygge en sammen i denne artikkelen....

Lys, kamera ... vent, vi glemte rekvisittene!

Vi skal selvfølgelig ikke forholde oss til den "ekte varen" - å forklare selv litt av det vi bygger er langt utenfor rammene av et blogginnlegg. Vi kan bygge noe raskt og enkelt som erstatning for de aktuelle bibliotekene, og deretter fokusere mer på Ruby-delen.

For det første trenger vi noe som kan erstatte det eksterne biblioteket vi har med å gjøre. Vi trenger at det skal ha et par egenskaper: det skal kontakte en ekstern tjeneste, sende ut en hendelse her og der, og mest av alt - ikke være bygget med tanke på enkel integrering 🙂 ...

Her er det vi skal bruke:

/* 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: {}
}

Du vil legge merke til at den kaller et åpent API for noen data - i dette tilfellet noen valutakurser for kryptovaluta, siden det er det som er i vinden for tiden. Dette API-et eksponerer ikke en sandkasse, og det er hastighetsbegrenset, noe som gjør det til et godt eksempel på noe som faktisk ikke bør bli truffet i tester.

Du vil kanskje legge merke til at dette faktisk er en NPM-kompatibel modul, mens jeg har antydet at skriptet vi vanligvis håndterer ikke er tilgjengelig på NPM for enkel bundling. For denne demonstrasjonen er det nok at det utviser en viss oppførsel, og jeg vil heller ha enkel forklaring her på bekostning av overforenkling.

Inviterer skuespillerne

Nå trenger vi også noe som kan erstatte biblioteket vårt. Igjen holder vi kravene enkle: det må kalle det "eksterne" biblioteket vårt og gjøre noe med utdataene. For å holde den "testbare" delen enkel, vil vi også la den utføre dobbel logging: både til konsollen, som er litt vanskeligere å lese i spesifikasjonene, og til en globalt tilgjengelig matrise.

window.remote = require('remote-calling-example')

window.failedMiserably = true
window.logs = []

function log (melding) {
window.logs.push(melding)
console.log(melding)
}

window.addEventListener('example:fetched', function () {
if (window.remote.error) {
log('[EKSEMPEL] Ekstern henting mislyktes')
window.failedMiserably = true
} else {
log('[EKSEMPEL] Ekstern henting vellykket')
log([EKSEMPEL] BTC til ETH: ${window.remote.data.BTC_ETH.last})
}
})

window.remote.fetch()

Jeg holder også oppførselen svimlende enkel med vilje. Som det er nå, er det bare to interessante kodebaner å spesifisere for, slik at vi ikke blir feid under et skred av spesifikasjoner etter hvert som vi utvikler oss gjennom bygget.

Alt bare klikker sammen

Vi legger ut en enkel HTML-side:

<code> <!DOCTYPE html>
 <html>
 <head>
   <title>Eksempelside</title>
   <script type="text/javascript" src="./index.js"></script>
 </head>
 <body></body>
 </html>

I denne demoen pakker vi HTML og JavaScript sammen med Parsellen veldig enkel web-app bundler. Jeg liker Parcel veldig godt i slike situasjoner, når jeg kaster sammen et raskt eksempel eller hacker på en idé til en klasse. Når du gjør noe så enkelt at det ville tatt lengre tid å konfigurere Webpack enn å skrive koden du ønsker, er det det beste.

Det er også diskret nok til at når jeg vil bytte til noe som er litt mer kamptestet, trenger jeg ikke å gjøre nesten noen backpedaling fra Parcel, noe som ikke er noe du kan si om Webpack. Parcel er imidlertid i tung utvikling, og problemer kan og vil dukke opp; jeg har hatt et problem der den transpilerte JavaScript-utgangen var ugyldig på en eldre Node.js. Poenget er at du ikke bør gjøre det til en del av produksjonspipelinen din ennå, men prøv det likevel.

Utnytt kraften i integrering

Nå kan vi konstruere testnettet vårt.

For selve spesifikasjonsrammeverket har vi brukt rspec. I utviklingsmiljøer tester vi med faktisk, ikke-headless Chrome - oppgaven med å kjøre og kontrollere det har falt på watir (og dens trofaste følgesvenn watir-rspec). Som stedfortreder har vi invitert Puffing Billy og stativ til festen. Til slutt ønsker vi å kjøre JavaScript-byggingen vår på nytt hver gang vi kjører spesifikasjonene, og det oppnås med kokain.

Det er en hel haug med bevegelige deler, og derfor er spesifikasjonshjelperen vår noe... komplisert, selv i dette enkle eksempelet. La oss ta en titt på den og plukke den fra hverandre.

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

Før hele suiten kjører vi vår egendefinerte byggekommando gjennom cocaine. Den TEST_LOGGER-konstanten er kanskje litt i overkant, men vi er ikke veldig opptatt av antall objekter her. Vi kjører selvfølgelig specs i tilfeldig rekkefølge, og vi trenger å inkludere alle godbitene fra watir-rspec. Vi må også sette opp Billy slik at den ikke cacher, men utstrakt logging til spec/log/billy.log. Hvis du ikke vet om en forespørsel faktisk blir stubbet eller om den treffer en live-server (ups!), er denne loggen gull verdt.

Jeg er sikker på at dine skarpe øyne allerede har oppdaget ProxySupport og BrowserSupport. Du tror kanskje at våre egne godbiter sitter der inne ... og det har du helt rett i! La oss se hva BrowserSupport gjør først.

En nettleser, kontrollert

La oss først introdusere TempBrowser:

klasse 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'
options.addargument "--proxy-server=#{Billy.proxy.host}:#{Billy.proxy.port}"
end
end
end

Når vi jobber oss bakover gjennom anropstreet, ser vi at vi setter opp et Selenium-nettleseralternativsett for Chrome. Ett av alternativene vi sender inn i det, er avgjørende for oppsettet vårt: Det instruerer Chrome-forekomsten om å sende alt gjennom Puffing Billy-forekomsten vår. Det andre alternativet er bare hyggelig å ha - hver forekomst vi kjører som ikke er hodeløs åpnes inspeksjonsverktøyene automatisk. Det sparer oss for utallige Cmd+Alt+I per dag 😉.

Etter at vi har konfigurert nettleseren med disse alternativene, sender vi den videre til Watir, og det er stort sett alt. Den drepe metoden er en liten finesse som gjør at vi kan stoppe og starte driveren gjentatte ganger hvis vi trenger det, uten å kaste TempBrowser-instansen.

Nå kan vi gi rspec-eksemplene våre et par superkrefter. Først og fremst får vi en smart nettleser hjelpemetoden som spesifikasjonene våre for det meste vil dreie seg om. Vi kan også benytte oss av en hendig metode for å starte nettleseren på nytt for et bestemt eksempel hvis vi gjør noe veldig sensitivt. Vi ønsker selvfølgelig også å drepe nettleseren etter at testsuiten er ferdig, for vi ønsker under ingen omstendigheter å ha Chrome-instanser som henger igjen - av hensyn til RAM-minnet vårt.

modul BrowserSupport
def self.browser
@browser ||= TempBrowser.new
end

def self.configure(config)
config.around(:each) do |example|
BrowserSupport.browser.kill if eksempel.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

Koble opp proxyen

Vi har satt opp en nettleser og spec-hjelpere, og vi er klare til å begynne å sende forespørsler til proxyen vår. Men vent, vi har ikke satt den opp ennå! Vi kunne ha laget gjentatte kall til Billy for hvert eneste eksempel, men det er bedre å skaffe oss et par hjelpemetoder og spare et par tusen tastetrykk. Det er det ProxySupport gjør det.

Den vi bruker i testoppsettet vårt, er litt mer kompleks, men her er en generell idé:

frozenstringliteral: true

krever 'json'

modul 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, fil)
Billy.proxy.stub(url).andreturn({
body: open(file).read,
code: 200,
headers: HEADERS.dup
})
slutt

def stubstatus(url, status)
Billy.proxy.stub(url).andreturn({
body: '',
code: status,
headers: HEADERS.dup
})
end

def stubpage(url, fil)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'text/html',
code: 200
)
end

def stubjs(url, fil)
Billy.proxy.stub(url).andreturn(
body: open(file).read,
content_type: 'application/javascript',
code: 200
)
end
slutt

Vi kan stubbe:

  • HTML-sideforespørsler - for hovedsiden vår "lekeplass",
  • JS-forespørsler - for å betjene det samlede biblioteket vårt,
  • JSON-forespørsler - for å stubbe forespørselen til det eksterne API-et,
  • og bare en "hva som helst"-forespørsel der vi bare bryr oss om å returnere et bestemt HTTP-svar som ikke er 200.

Dette vil fungere fint for vårt enkle eksempel. Apropos eksempler - vi bør sette opp et par!

Tester den gode siden

Vi må først koble sammen et par "ruter" for proxyen vår:

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

Det er verdt å merke seg at fra rspecs perspektiv refererer de relative stiene her til hovedprosjektkatalogen, så vi laster inn HTML og JS direkte fra distanse katalogen - slik den er bygget av Parcel. Du kan allerede se hvordan disse stub_* hjelpere kommer godt med.

Det er også verdt å merke seg at vi plasserer vårt "falske" nettsted på en .lokal TLD. På den måten bør ikke eventuelle løpske forespørsler unnslippe vårt lokale miljø hvis noe skulle gå galt. Som en generell praksis vil jeg anbefale å i det minste ikke bruke "ekte" domenenavn i stubber med mindre det er absolutt nødvendig.

En annen ting vi bør merke oss her, er at vi ikke bør gjenta oss selv. Etter hvert som proxy-rutingen blir mer kompleks, med mange flere stier og nettadresser, vil det være verdifullt å trekke ut dette oppsettet til en delt kontekst og bare inkludere det etter behov.

Nå kan vi finne ut hvordan den "gode" stien vår skal se ut:

context 'med riktig svar' 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 'logger riktige data' do
expect(browser.execute_script('return window.logs')).to(
eq([['[EKSEMPEL] Ekstern henting vellykket', '[EKSEMPEL] BTC til ETH: 0,03619999'])
)
end
end

Det er ganske enkelt, ikke sant? Litt mer oppsett her - vi stubber JSON-svaret fra det eksterne API-et med en fixture, går til hoved-URL-en vår, og så... venter vi.

Den lengste ventetiden

Ventetidene er en måte å omgå en begrensning vi har støtt på med Watir - vi kan ikke vente pålitelig på f.eks. JavaScript-hendelser, så vi må jukse litt og "vente" til skriptene har flyttet et objekt som vi kan få tilgang til, til en tilstand som er av interesse for oss. Ulempen er at hvis denne tilstanden aldri kommer (for eksempel på grunn av en feil), må vi vente på at watir-venteren skal ta timeout. Dette øker spesifikasjonstiden litt. Spesifikasjonen mislykkes likevel på en pålitelig måte.

Etter at siden har "stabilisert" seg i den tilstanden vi er interessert i, kan vi utføre litt mer JavaScript i sammenheng med siden. Her henter vi opp loggene som er skrevet til den offentlige matrisen, og sjekker om de er som forventet.

Som en sidebemerknad - det er her stubbing av den eksterne forespørselen virkelig skinner. Svaret som logges på konsollen, er avhengig av valutakursen som returneres av det eksterne API-et, så vi kan ikke teste innholdet i loggen på en pålitelig måte hvis det stadig endres. Det finnes selvfølgelig måter å omgå dette på, men de er ikke særlig elegante.

Testing av den dårlige grenen

En ting til å teste: "failure"-grenen.

context 'med mislykket svar' do
before do
stubstatus %r{http://poloniex.com/public(.*)}, 404
goto pageurl
Watir::Wait.until { browser.execute_script('return window.logs.length === 1') }
end

it 'logger feil' do
expect(browser.execute_script('return window.logs')).to(
eq([['[EKSEMPEL] Fjernhenting mislyktes'])
)
end
end

Det ligner veldig på det ovennevnte, med den forskjellen at vi stubber svaret slik at det returnerer en 404 HTTP-statuskode og forventer en annen logg.

La oss kjøre spesifikasjonene våre nå.

%-pakke exec rspec
Randomisert med frø 63792
I, [2017-12-21T14:26:08.680953 #7303] INFO -- : Kommando :: npm run build

Eksternt anrop
med riktig svar
logger riktige data
med mislykket svar
logger feil

Ferdig på 23,56 sekunder (filene brukte 0,86547 sekunder på å lastes inn)
2 eksempler, 0 feil

Woohoo!

Konklusjon

Vi har kort diskutert hvordan JavaScript kan integrasjonstestes med Ruby. Selv om det opprinnelig ble sett på som en nødløsning, er vi ganske fornøyde med den lille prototypen vår nå. Vi vurderer selvfølgelig fortsatt en ren JavaScript-løsning, men i mellomtiden har vi en enkel og praktisk måte å reprodusere og teste noen svært komplekse situasjoner som vi har støtt på ute i det fri.

Hvis du vurderer å bygge noe lignende selv, bør det bemerkes at det ikke er uten begrensninger. Hvis det du tester for eksempel blir veldig AJAX-tungt, vil Puffing Billy bruke lang tid på å svare. Hvis du må stubbe noen SSL-kilder, kreves det også litt mer fikling - se i watir-dokumentasjonen hvis det er et krav du har. Vi kommer helt sikkert til å fortsette å utforske og lete etter de beste måtene å håndtere vårt unike brukstilfelle på - og vi skal sørge for å fortelle deg hva vi har funnet ut.

Relaterte artikler

Programvareutvikling

Bygg fremtidssikre webapper: Innsikt fra The Codests ekspertteam

Oppdag hvordan The Codest utmerker seg når det gjelder å skape skalerbare, interaktive webapplikasjoner med banebrytende teknologi som gir sømløse brukeropplevelser på tvers av alle plattformer. Finn ut hvordan ekspertisen vår driver digital transformasjon og...

THECODEST
Programvareutvikling

Topp 10 Latvia-baserte programvareutviklingsselskaper

I vår nyeste artikkel kan du lese mer om Latvias beste programvareutviklingsselskaper og deres innovative løsninger. Oppdag hvordan disse teknologilederne kan bidra til å løfte virksomheten din.

thecodest
Løsninger for bedrifter og oppskalering

Grunnleggende om Java-programvareutvikling: En guide til vellykket outsourcing

Utforsk denne viktige veiledningen om vellykket outsourcing av Java-programvareutvikling for å øke effektiviteten, få tilgang til ekspertise og drive frem prosjektsuksess med The Codest.

thecodest
Programvareutvikling

Den ultimate guiden til outsourcing i Polen

Den kraftige økningen i outsourcing i Polen er drevet av økonomiske, utdanningsmessige og teknologiske fremskritt, noe som fremmer IT-vekst og et forretningsvennlig klima.

TheCodest
Løsninger for bedrifter og oppskalering

Den komplette guiden til verktøy og teknikker for IT-revisjon

IT-revisjoner sørger for sikre, effektive og kompatible systemer. Les hele artikkelen for å lære mer om viktigheten av dem.

The Codest
Jakub Jakubowicz CTO og medgrunnlegger

Abonner på vår kunnskapsbase og hold deg oppdatert på ekspertisen fra IT-sektoren.

    Om oss

    The Codest - Internasjonalt programvareutviklingsselskap med teknologisentre i Polen.

    Storbritannia - Hovedkvarter

    • Kontor 303B, 182-184 High Street North E6 2JA
      London, England

    Polen - Lokale teknologisentre

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warszawa, Polen

      The Codest

    • Hjem
    • Om oss
    • Tjenester
    • Casestudier
    • Vet hvordan
    • Karriere
    • Ordbok

      Tjenester

    • Det rådgivende
    • Programvareutvikling
    • Backend-utvikling
    • Frontend-utvikling
    • Staff Augmentation
    • Backend-utviklere
    • Ingeniører i skyen
    • Dataingeniører
    • Annet
    • QA-ingeniører

      Ressurser

    • Fakta og myter om samarbeid med en ekstern programvareutviklingspartner
    • Fra USA til Europa: Hvorfor velger amerikanske oppstartsbedrifter å flytte til Europa?
    • Sammenligning av Tech Offshore Development Hubs: Tech Offshore Europa (Polen), ASEAN (Filippinene), Eurasia (Tyrkia)
    • Hva er de største utfordringene for CTO-er og CIO-er?
    • The Codest
    • The Codest
    • The Codest
    • Retningslinjer for personver
    • Vilkår for bruk av nettstedet

    Opphavsrett © 2025 av The Codest. Alle rettigheter forbeholdt.

    nb_NONorwegian
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek nb_NONorwegian