Tehtävät
Neljännen osan tavoitteet

Osaa jakaa sovelluksen osia sovelluksen sisäisiksi palveluiksi ("Service"). Ymmärtää tarpeen web-sovellusten kehitysprosessin automatisoinnille sekä web-sovellusten jatkuvalle inkrementaaliselle kehitykselle. Ymmärtää versionhallintapalvelun (esim. Github), sovelluksen testejä suorittavan integraatiopalvelun (esim. Travis CI) sekä testi- ja tuotantoympäristön (esim. Heroku) yhteistyön.

Sovelluksen rakenne ja pyynnön kulku sovelluksessa

Web-sovellusten suunnittelussa noudatetaan useita arkkitehtuurimalleja. Tyypillisimpiä näistä ovat MVC-arkkitehtuuri sekä kerrosarkkitehtuuri. Kummassakin perusperiaatteena on vastuiden jako selkeisiin osakokonaisuuksiin.

MVC-arkkitehtuuri

MVC-arkkitehtuurin tavoitteena on käyttöliittymän erottaminen sovelluksen toiminnasta siten, että käyttöliittymät eivät sisällä sovelluksen toiminnan kannalta tärkeää sovelluslogiikkaa. MVC-arkkitehtuurissa ohjelmisto jaetaan kolmeen osaan: malliin (model, tiedon tallennus- ja hakutoiminnallisuus), näkymään (view, käyttöliittymän ulkoasu ja tiedon esitystapa) ja käsittelijään (controller, käyttäjältä saatujen käskyjen käsittely sekä sovelluslogiikka).

MVC-malli yhdistetään tyypillisesti työpöytäsovelluksiin, missä käsittelijä voi olla jatkuvassa yhteydessä näkymään ja malliin. Tällöin käyttäjän yksittäinen toiminta käyttöliittymässä -- esimerkiksi tekstikentän tiedon päivitys -- liittyy tapahtumankäsittelijään, joka ohjaa tiedon malliin liittyvälle ohjelmakoodille, jonka tehtävänä on päivittää sovellukseen liittyvää tietoa tarvittaessa. Tapahtumankäsittelijä mahdollisesti sisältää myös ohjelmakoodia, joka pyytää muunnosta käyttöliittymässä.

Web-sovelluksissa käsittelijän ohjelmakoodia suoritetaan vain kun selain lähettää palvelimelle pyynnön. Ohjelmakoodissa haetaan esimerkiksi tietokannasta tietoa, joka ohjataan näkymän luontiin tarkoitetulle sovelluksen osalle. Kun näkymä on luotu, palautetaan se pyynnön tehneelle selaimelle. Spring-sovelluksissa kontrollereissa näkyvä Model viittaa tietoon, jota käytetään näkymän luomisessa -- se ei kuitenkaan vastaa MVC-mallin termiä model, joka liittyy kattavammin koko tietokantatoiminnallisuuteen.

MVC-mallissa käyttäjän pyyntö ohjautuu kontrollerille, joka sisältää sovelluslogiikkaa. Kontrolleri kutsuu pyynnöstä riippuen mallin toiminnallisuuksia ja hakee sieltä esimerkiksi tietoa. Tämän jälkeen pyyntö ohjataan näkymän luomisesta vastuulle olevalle komponentilla ja näkymä luodaan. Lopulta näkymä palautetaan vastauksena käyttäjän tekemälle pyynnölle.

MVC-mallista on useita hyötyjä. Käyttöliittymien (näkymien) suunnittelu ja toteutus voidaan eriyttää sovelluslogiikan toteuttamisesta, jolloin niitä voidaan työstää rinnakkain. Samalla ohjelmakoodi selkenee, sillä eri komponenttien vastuut ovat eriteltyjä -- näkymät eivät sisällä sovelluslogiikkaa, kontrollerin tehtävänä on käsitellä pyynnöt ja ohjata niitä eteenpäin, ja mallin vastuulla on tietoon liittyvät operaatiot. Tämän lisäksi sovellukseen voidaan luoda useampia käyttöliittymiä, joista jokainen käyttää samaa sovelluslogiikkaa, ja pyynnön kulku sovelluksessa selkiytyy.

Kerrosarkkitehtuuri

Kun sovellus jaetaan selkeisiin vastuualueisiin, selkeytyy myös pyynnön kulku sovelluksessa. Kerrosarkkitehtuuria noudattamalla pyritään tilanteeseen, missä sovellus on jaettu itsenäisiin kerroksiin, jotka toimivat vuorovaikutuksessa muiden kerrosten kanssa. Käyttöliittymäkerros sisältää näkymät (esim. Thymeleafin html-sivut) sekä mahdollisen logiikan tiedon näyttämiseen (esim tägit html-sivuilla). Käyttöliittymä näkyy käyttäjän selaimessa, ja käyttäjän selain tekee palvelimelle pyyntöjä käyttöliittymässä tehtyjen klikkausten ja muiden toimintojen pohjalta. Palvelimella toimivan sovelluksen kontrollerikerros ottaa vastaan nämä pyynnöt, ja ohjaa ne eteenpäin sovelluksen sisällä.

Spring-sovellusten yhteydessä kerrosarkkitehtuurilla tarkoitetaan yleisesti ottaen seuraavaa jakoa:

  • Käyttöliittymäkerros
  • Kontrollerikerros
  • Sovelluslogiikka ja palvelut
  • Tallennuslogiikka (tietokanta-abstraktio ja tietokantapalvelut)

Kerrosarkkitehtuuria noudattaessa ylempi kerros hyödyntää alemman kerroksen tarjoamia toiminnallisuuksia, mutta alempi kerros ei hyödynnä ylempien kerrosten tarjoamia palveluita. Puhtaassa kerrosarkkitehtuurissa kaikki kerrokset ovat olemassa, ja kutsut eivät ohita kerroksia ylhäältä alaspäin kulkiessaan. Tällä kurssilla noudatamme avointa kerrosarkkitehtuuria, missä kerrosten ohittaminen on sallittua -- jos sovelluksen ylläpidettävyys ja rakenne ei sitä kiellä, voi myös Repository-rajapinnan toteuttavat oliot sisällyttää kontrollereihin.

Kerrosarkkitehtuurissa sovelluksen vastuut jaetaan kerroksittain. Näkymäkerros sisältää käyttöliittymät, joista voidaan tehdä pyyntöjä kontrollerille. Kontrolleri käsittelee palveluita, jotka ovat yhteydessä tallennuslogiikkaan. Tiedon tallentamiseen käytettäviä entiteettejä sekä muita luokkia (esim "view objects") käytetään kaikilla kerroksilla.

Kontrollerikerros

Kontrollerien ensisijaisena vastuuna on pyyntöjen kuuntelu, pyyntöjen ohjaaminen sopiville palveluille, sekä tuotetun tiedon ohjaaminen oikealle näkymälle tai näkymän generoivalle komponentille.

Jotta palveluille ei ohjata epäoleellista dataa, esimerkiksi huonoja arvoja sisältäviä parametreja, on kontrolleritason vastuulla myös pyynnössä olevien parametrien validointi.

Kontrollerikerroksen luokissa käytetään annotaatiota @Controller, ja luokkien metodit, jotka vastaanottavat pyyntöjä annotoidaan esimerkiksi @GetMapping- ja @PostMapping-annotaatioilla.

Palvelukerros

Palvelukerros tarjoaa kontrollerikerrokselle palveluita, joita kontrollerikerros voi käyttää. Palvelut voivat esimerkiksi abstrahoida kolmannen osapuolen tarjoamia komponentteja tai rajapintoja, tai sisältää toiminnallisuutta, jonka toteuttaminen kontrollerissa ei ole järkevää esimerkiksi sovelluksen ylläpidettävyyden kannalta.

Palvelukerroksen luokat merkitään annotaatiolla @Service tai @Component. Tämä annotaatio tarkoittaa käytännössä sitä, että sovelluksen käynnistyessä luokasta tehdään olio, joka ladataan sovelluksen muistiin. Tämän jälkeen jokaiseen luotavaan olioon, jonka luokassa on @Autowired-annotaatiolla merkitty oliomuuttuja, sisällytetään muistiin ladattu olio.

Tarkastellaan toisella viikolla ollutta tehtävää Bank Transfer sekä sen erästä mahdollista ratkaisua. Tehtävässä tavoitteena oli luoda sovellus, joka voi tehdä tilisiirron parametreina annettujen tilien välillä. Eräs ratkaisu on seuraavanlainen.

@Controller
public class BankingController {
 
    @Autowired
    private AccountRepository accountRepository;
 
    @GetMapping("/")
    public String list(Model model) {
        model.addAttribute("accounts", this.accountRepository.findAll());
        return "index";
    }
 
    @PostMapping("/")
    public String transfer(@RequestParam String from, @RequestParam String to,
                           @RequestParam Integer amount) {
        Account accountFrom = this.accountRepository.findByIban(from);
        Account accountTo = this.accountRepository.findByIban(to);
 
        accountFrom.setBalance(accountFrom.getBalance() - amount);
        accountTo.setBalance(accountTo.getBalance() + amount);
 
        this.accountRepository.save(accountFrom);
        this.accountRepository.save(accountTo);
 
        return "redirect:/";
    }
}

Yllä olevassa esimerkissä kontrolleri on jo melko raskas. Eräs vaihtoehto on luoda erillinen luokka BankingService, jolle määritellään metodi transfer. Metodi saa parametrina kaksi tiliä sekä summan.

@Service
public class BankingService {
 
    @Autowired
    private AccountRepository accountRepository;
  
    @Transactional
    public void transfer(String from, String to, Integer amount) {
        Account accountFrom = this.accountRepository.findByIban(from);
        Account accountTo = this.accountRepository.findByIban(to);
 
        accountFrom.setBalance(accountFrom.getBalance() - amount);
        accountTo.setBalance(accountTo.getBalance() + amount);
    }
}

Nyt Controller-luokan toiminnallisuus kevenee hieman.

@Controller
public class BankingController {
 
    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private BankingService bankingService;
 
    @GetMapping("/")
    public String list(Model model) {
        model.addAttribute("accounts", this.accountRepository.findAll());
        return "index";
    }
 
    @PostMapping("/")
    public String transfer(@RequestParam String from, @RequestParam String to,
                           @RequestParam Integer amount) {
        this.bankingService.transfer(from, to, amount);
        return "redirect:/";
    }
}

Yllä kontrolleriluokan metodi transfer on selkeä ja sen vastuulla on vain pyynnön vastaanottaminen sekä tehtävien delegointi. Jatkossa myös muut tilisiirtoa mahdollisesti tarvitsevat sovelluksen osat voivat hyödyntää suoraan BankingService-luokassa määriteltyä metodia.

Tallennuslogiikka

Tallennuslogiikkakerros sisältää tietokannan käyttöön liittyvät oleelliset oliot. Pankki saattaisi tarvita esimerkiksi Tilitapahtumiin liittyvää tallennuslogiikkaa. Täällä olisi esimerkiksi Repository-rajapinnat, jotka perivät rajapinnan JpaRepository.

Tietoa sisältävät oliot

Tiedon esittämiseen liittyvät oliot elävät kerrosarkkitehtuurissa kerrosten sivulla. Esimerkiksi entiteettejä voidaan käsitellä tallennuslogiikkakerroksella (tiedon tallennus), palvelukerroksella (tiedon käsittely), kontrollerikerroksella (tiedon lisääminen Model-olioon) sekä näkymäkerroksella (Model-olion käyttäminen näkymän luomiseen.

Sovellusten kehittämisessä näkee välillä myös jaon useampaan erilaiseen tietoa sisältävään oliotyyppiin. Entiteettejä käytetään tietokantatoiminnallisuudessa, mutta välillä näkymien käsittelyyn palautettavat oliot pidetään erillisinä entiteeteistä. Tähän ei ole oikeastaan yhtä oikeaa tapaa: lähestymistapa valitaan tyypillisesti ohjelmistokehitystiimin kesken.

Dependency Injection ja Inversion of Control

Jokaisella luokalla on oma selkeä vastuualueensa, ja vastuiden sekoittamista tulee välttää. Inversion of Control ja Dependency Injection ovat suunnitelumalleja, joilla pyritään vähentämään olioiden turhia riippuvuuksia.

Perinteisissä ohjelmistoissa olioiden luominen on ohjelmoijan vastuulla, mutta ohjelmistokehykset tekevät osan tästä työstä puolestamme.

Spring luo käyttöömme luokkia joita tarvitsemme: Kontrollin käännöllä tarkoitetaan ohjelman toiminnan hallinnan vastuun siirtämistä sovelluskehykselle ja ohjelmaa suorittavalle palvelimelle (inversion of control).

Spring injektoi @Autowired-annotaatiolla merkittyihin oliomuuttujiin ilmentymät luokista, jotka se lataa käyttöönsä palvelinohjelmiston käynnistyessä (dependency injection). Luokat ladataan annotaatioiden perusteella: jos luokalla on @Controller, @Service, @Component tai @Repository -annotaatio, ladataan se ohjelman käyttöön alussa.

Lue lisää aiheesta Martin Fowlerin artikkelista.

Tehtäväpohjassa on sovellus, joka hakee vitsejä verkkopalvelusta. Sovelluksessa on myös toiminnallisuus vitsien hyvyyden äänestämiseen. Muokkaa sovellusta siten, että vitsien äänestystoiminnallisuus eriytetään omaan Service-luokkaan, jolloin mm. luokan JokeController vastuualueet selkeytyvät.

Konfiguraatioprofiilit

Ohjelmistotuotannossa jokaisella ohjelmistokehittäjällä on oma ympäristö, missä sovellusta voi kehittää ja testata. Sovelluksen lähdekoodi sijaitsee versionhallintapalvelussa, ja sovelluksen toiminta ei ole riippuvainen työympäristöstä -- sovelluksen siirtämisen koneelta toiselle ei lähtökohtaisesti pitäisi vaatia muutoksia ohjelman lähdekoodiin. Samanlaista joustavuutta odotetaan myös silloin kun sovelluksesta julkaistaan uusi versio käyttäjille.

Sovelluksen julkaisun eli esimerkiksi tuotantopalvelimelle siirtämisen ei tule vaatia muutoksia sovelluksen lähdekoodiin. Kun sovellus on julkisessa käytössä, sillä on tyypillisesti ainakin usein eri tietokantaosoite kuin sovelluskehitysvaiheessa, mahdollisesti eri tietokannanhallintajärjestelmä, sekä todennäköisiä erilaisia salasanoihin ja ohjelman tuottamiin tulostuksiin (logeihin) liittyviä asetuksia.

Tarvitsemme siis tavan olennaisten asetusten määrittelyyn ympäristökohtaisesti.

Spring-projekteissa konfiguraatiotiedostot sijaitsevat tyypillisesti kansiossa src/main/resources/. Spring etsii kansiosta tiedostoa nimeltä application.properties, johon ohjelmistokehittäjä voi määritellä sovelluksen käynnistyksen yhteydessä käytettäviä asetuksia. Asetustiedosto voi sisältää esimerkiksi tietokantaan liittyviä asetuksia:

spring.datasource.driverClassName=tietokanta-ajuri
spring.datasource.url=jdbc-osoite
spring.jpa.show-sql=true

Ensimmäiset kaksi kertovat tietokannanhallintajärjestelmän käyttämän ajurin sekä osoitteen, kolmas pyytää sovellusta kirjoittamaan tietokantakyselyt lokiin.

Lista tyypillisistä asetuksista löytyy osoitteesta https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html.

Käytettävän profiilin ja konfiguraatiotiedoston vaihtaminen toteutetaan tyypillisesti ympäristömuuttujan avulla. Ympäristömuuttuja (SPRING_PROFILES_ACTIVE) kertoo käytettävän profiilin. Ympäristömuuttujan voi antaa myös sovellukselle parametrina sovellusta käynnistettäessä (java ... -Dspring.profiles.active=arvo ...).

Jos käytössä on aktiivista profiilia kuvaava ympäristömuuttuja, etsii Spring oletuskonfiguraatiotiedoston (application.properties) lisäksi myös aktiiviseen profiiliin liittyvää konfiguraatiotiedostoa. Jos aktiivisena profiilina on production, etsitään myös konfiguraatiotiedostoa application-production.properties. Konfiguraatioprofiili voisi esimerkiksi sisältää tietoa käytettävästä tietokanta-ajurista sekä tietokannan osoitteesta.

Sovelluksen siirtäminen pilvipalveluun

Tutustutaan seuraavaksi sovelluksen siirtämiseen Heroku-pilvipalveluun. Heroku on palvelu, joka tarjoaa rajoitetun (ja ilmaisen) sijoituspaikan vähän resursseja kuluttaville sovelluksille. Toisin kuin aiemmin toteuttamiemme sovellusten tietokanta, Herokun käyttämä tietokannanhallintajärjestelmä on erillinen sovelluksesta, jolloin tietokantaan tallennetut tiedot pysyvät tietokannassa vaikka sovellus sammuisi välillä.

Seuraa ensin osoitteessa https://devcenter.heroku.com/articles/deploying-spring-boot-apps-to-heroku olevaa opasta Spring Boot -sovelluksen käytöstä Herokussa ja luo ensimmäinen pilvessä sijaitseva Heroku-sovelluksesi.

Jotta saisimme oman tietokantaa käyttävän sovelluksen Herokuun, tarvitsemme ajurin Herokun käyttämään PostgreSQL-tietokannanhallintajärjestelmään. Tämän saa käyttöön lisäämällä projektin pom.xml-tiedostoon seuraavan riippuvuuden.

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

Luodaan seuraavaksi konfiguraatiotiedosto, jolla määrittelemme sovelluksen käyttöön PostgreSQL-kielen sekä pyydämme tietokantakyselyitä näkyville. Seuraava sisältö tulee tiedostoon src/main/resources/application-production.properties. Alla määritellyt parametrit kuten ${JDBC_DATABASE_URL} tulevat Herokulta automaattisesti sovelluksen käynnistyksen yhteydessä.

spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=${JDBC_DATABASE_URL}
spring.datasource.jdbcUrl=${JDBC_DATABASE_URL}
spring.datasource.username=${JDBC_DATABASE_USERNAME}
spring.datasource.password=${JDBC_DATABASE_PASSWORD}
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

Luodaan tämän jälkeen profiili tuotantokäyttöä varten. Profiili noudattaa Herokun opasta osoitteessa https://devcenter.heroku.com/articles/connecting-to-relational-databases-on-heroku-with-java, mutta on käytössä vain profiililla production. Tämän avulla sovellus muuntaa Herokun antaman tietokantaosoitteen sovelluksen käyttöön.

// pakkaus

@Configuration
@Profile("production")
public class ProductionProfile {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
}

Luodaan lopulta vielä erillinen Procfile-tiedosto, jonka perusteella Heroku osaa käynnistää sovelluksen. Procfile-tiedoston sisältö on seuraava:

web: java $JAVA_OPTS -Dspring.profiles.active=production -Dserver.port=$PORT -jar target/*.jar

Tämän jälkeen sovelluksen siirtäminen tuotantoon onnistuu alkuperäisiä Herokun ohjeita noudattamalla.

Heroku määrittelee sovellukselle käynnistysparametrit sekä portin, jonka lisäksi määrittelemme aktiiviseksi profiiliksi tuotantoprofiilin. Kun sovellus siirretään herokuun, se käyttää Herokun tietokantaa. Toisaalta, kun sovellusta kehitetään paikallisesti, käytössä on testitietokanta -- ihan näppärää.

Voit kokeilla ReloadHeroes-sovellusta osoitteessa https://still-beyond-90359.herokuapp.com/.

PostgreSQL ja Heroku

Jos Herokun PostgreSQL ei lähde edellä kuvatulla esimerkillä käyntiin, tarkasta vielä, että sovellukseen on lisätty Herokussa tietokannanhallintajärjestelmä. Tämä löytyy Herokusta sovelluksen Resources-välilehdeltä kohdasta Add-ons.

Sovellusten testaaminen

Sovellusten testaaminen helpottaa sekä kehitystyötä että tulevaa ylläpitotyötä. Testaaminen voidaan karkeasti jakaa kolmeen osaan: yksikkötestaukseen, integraatiotestaukseen ja järjestelmätestaukseen. Tämän lisäksi on mm. myös käytettävyys- ja tietoturvatestaus, joita emme tässä käsittele tarkemmin.

Yksikkötestauksessa tarkastellaan sovellukseen kuuluvia yksittäisiä komponentteja ja varmistetaan että niiden tarjoamat rajapinnat toimivat tarkoitetulla tavalla. Integraatiotestauksessa pyritään varmistamaan, että komponentit toimivat yhdessä kuten niiden pitäisi. Järjestelmätestauksessa varmistetaan, että järjestelmä toimii vaatimusten mukaan järjestelmän käyttäjille tarjotun rajapinnan (esim. selain) kautta.

Yksikkötestaus

Yksikkötestauksella tarkoitetaan lähdekoodiin kuuluvien yksittäisten osien testausta. Termi yksikkö viittaa ohjelman pienimpiin mahdollisiin testattaviin toiminnallisuuksiin, kuten olion tarjoamiin metodeihin. Seuratessamme single responsibility principleä, jokaisella oliolla ja metodilla on yksi selkeä vastuu, jota voi myös testata. Testaus tapahtuu yleensä testausohjelmistokehyksen avulla, jolloin luodut testit voidaan suorittaa automaattisesti. Yleisin Javalla käytettävä testauskehys on JUnit, jonka saa käyttöön lisäämällä siihen liittyvän riippuvuuden pom.xml-tiedostoon.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
</dependency>

Yksittäisen riippuvuuden määre scope kertoo milloin riippuvuutta tarvitaan. Määrittelemällä scope-elementin arvoksi test on riippuvuudet käytössä vain testejä ajettaessa. Uusia testiluokkia voi luoda NetBeansissa valitsemalla New -> Other -> JUnit -> JUnit Test. Tämän jälkeen NetBeans kysyy testiluokalle nimeä ja pakkausta. Huomaa että lähdekoodit ja testikoodit päätyvät erillisiin kansioihin -- juurin näin sen pitääkin olla. Kun testiluokka on luotu, on projektin rakenne kutakuinkin seuraavanlainen.

.
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- wad
    |   |       `-- ... oman projektin koodit
    |   |-- resources
    |           `-- ... resurssit, mm. konfiguraatio ja thymeleafin templatet
    |           
    `-- test
        `-- java
            `-- wad
                `-- ... testikoodit!

Tehtäväpohjissa JUnit-testikirjasto on valmiina mukana. Yksikkötestauksesta JUnit-kirjaston avulla löytyy pieni opas kurssin Ohjelmistotekniikan menetelmät sivuilta.

Integraatiotestaus

Spring tarjoaa spring-test-kirjaston, jonka avulla JUnit-kirjasto saa @Autowired-annotaatiot toimimaan. Tämän kautta pääsemme tilanteeseen, missä voimme injektoida testimetodille esimerkiksi repository-rajapinnan toteuttavan olion sekä testata sen tarjoamien metodien toimintaa. Testattava palvelu voi hyödyntää muita komponentteja, jolloin testauksen kohteena on kokonaisuuden toiminta yhdessä.

Spring test-komponentista on olemassa Spring Boot -projekti, jonka voimme ottaa käyttöömme lisäämällä seuraavan riippuvuuden pom.xml-tiedostoon. Käytetyn riippuvuuden versio liittyy Spring Boot -projektin versioon, eikä sitä tarvitse määritellä tarkemmin.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

Yksittäisten palvelujen testaamisessa tarvitsemme testiluokkien alkuun kaksi annotaatiota. Annotaatio @RunWith(SpringRunner.class) kertoo että käytämme Springiä yksikkötestien ajamiseen ja annotaatio @SpringBootTest lataa sovelluksen osat käyttöön.

Alla on esimerkkinä testiluokka, johon injektoidaan automaattisesti BankingService-olio sekä AccountRepository-rajapinnan toteuttama repository-olio.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {

    @Autowired
    private BankingService bankingService;

    @Autowired
    private AccountRepository accountRepository;

    // ... testit jne
}

Käynnistämällä Springin osana testejä, saamme käyttöömme oliokontekstin, jonka avulla voimme asettaa testattavat oliot testiluokkiin testaamista varten. Testattavien olioiden riippuvuudet asetetaan myös automaattisesti, eli jos BankingService sisältää muita komponentteja, on ne myös automaattisesti asetettu.

Voimme ylläolevalla lähestymistavalla testata myös sitä, että sovelluksemme eri osat toimivat yhteen toivotusti. Oletetaan, että käytössämme on aiemmin esitelty luokka BankingService, joka tarjoaa metodin transfer. Metodin pitäisi siirtää annettu summan kahden tilin välillä. Tämän lisäksi käytössämme on AccountRepository, jonka avulla voimme hakea tietokannasta tietoa tilien nimien perusteella.

Kummatkin toteutukset voidaan injektoida testiluokkaan. Alla oleva testi tarkastaa, että luokan BankingService toteutus toimii toivotulla tavalla.

// importit

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {

    @Autowired
    private BankingService bankingService;

    @Autowired
    private AccountRepository accountRepository;

    @Test
    public void testBankTransfer() {
        Account first = new Account();
        first.setBalance(200);
        first.setIban("first");
        Account second = new Account();
        second.setIban("second");
        second.setBalance(0);

        accontRepository.save(first);
        accontRepository.save(second);

        bankingService.transfer("first", "second", 200);

        assertEquals(0, accountRepository.findByIban("first").getBalance());
        assertEquals(200, accountRepository.findByIban("second").getBalance());
    }

    // ja muita testejä
}

Yllä oleva testi testaa vain tilisiirron onnistumista. Se ei kuitenkaan tarkasta esimerkiksi sivuvaikutuksia. Auki jäävät muunmuassa kysymykset: siirtääkö palvelu rahaa jollekin toiselle tilille? Mitä käy jos tilillä ei ole rahaa?

Järjestelmätestaus

Järjestelmätestauksessa pyritään varmistamaan, että järjestelmä toimii toivotulla tavalla. Järjestelmää testataan saman rajapinnan kautta, kuin mitä sen loppukäyttäjät käyttävät. Järjestelmätestaukseen on monenlaisia työkaluja, joista käsittelemme tässä kahta. Tutustumme ensin integraatiotestauksessa käytetyn spring-test-komponenttiin järjestelmätason testaustoiminnallisuuteen, jonka jälkeen tutustumme harjoitustehtävän kautta Selenium ja FluentLenium -kirjastoihin.

MockMvc

Springin tarjoama spring-test tarjoaa tuen järjestelmätestaamiseen. Annotaatiolla @SpringBootTest testeillä on käytössä myös web-sovelluksen konteksti, jonka avulla voidaan luoda MockMvc-olio. MockMvc-oliolla pystymme tekemään pyyntöjä sovelluksen tarjoamiin osoitteisiin, tarkistelemaan pyyntöjen onnistumista, sekä tarkastelemaan vastauksena saatua dataa.

Alla oleva esimerkki käynnistää sovelluksen ja tekee kolme GET-pyyntöä osoitteeseen /messages. Ensimmäinen pyyntö liittyy testiin, missä varmistetaan että vastaus on sisältää statuskoodin 200 eli "OK", toinen pyyntö liittyy testiin joka varmistaa että vastauksen tyyppi on JSON-muotoista dataa, ja kolmas pyyntö tarkistaa että vastauksessa on merkkijono "Awesome". Alun setUp-metodi luo MockMvc-olion injektoidun palveinkontekstin perusteella.

// muut importit

// mm. mockMvc:n get- ja post-metodit
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessagesTest {

    @Autowired
    private WebApplicationContext webAppContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
    }

    @Test
    public void statusOk() throws Exception {
        mockMvc.perform(get("/messages"))
                .andExpect(status().isOk());
    }


    @Test
    public void responseTypeApplicationJson() throws Exception {
        mockMvc.perform(get("/messages"))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON));
    }

    @Test
    public void responseContainsTextAwesome() throws Exception {
        MvcResult res = mockMvc.perform(get("/messages"))
                .andReturn();

        String content = res.getResponse().getContentAsString();
        Assert.assertTrue(content.contains("Awesome"));
    }
}

Voit myös testata modeliin asetettujen attribuuttien olemassaoloa ja oikeellisuutta. Olemassaolon voi tarkistaa model()-metodin kautta, ja MvcResult-olion kautta pääsee käsiksi modelin sisältöön.

@Test
public void modelHasAttributeMessages() throws Exception {
    mockMvc.perform(get("/messages"))
            .andExpect(model().attributeExists("messages"));
}

@Test
public void messagesCorrect() throws Exception {
    MvcResult res = mockMvc.perform(get("/messages"))
            .andReturn();

    // oletetaan, että kontrolleri asettaa kokoelman Message-tyyppisiä olioita
    // modeliin

    Collection<Message> messages = (Collection) res.getModelAndView().getModel().get("messages");

    // tarkista kokoelma
}

MockMvc:n avulla voi testata käytännössä suurinta osaa palvelinsovellusten toiminnallisuudesta, mutta samalla se tarjoaa pääsyn samaan rajapintaan kuin mitä selain käsitteelee.

Muistamme edellisestä osiosta tehtävän, missä tehtiin sovellus lentokoneiden ja lentokenttien hallintaan. Tässä tehtävässä harjoitellaan hieman sekä integraatio- että järjestelmätestausta.

Huom! Tässä tehtävässä ei ole automaattisia testejä, joilla testattaisiin kirjoittamiasi testejä. Palauttaessasi tehtävän olet tarkistanut, että kirjoittamasi testit toimivat kuten tehtävänannossa on kuvattu.

AirportServiceTest

Sovellusessa on luokka AirportService, mikä sijaitsee pakkauksessa wad.service. Sille ei kuitenkaan ole yhtäkään testiä :(

Lisää testikansioon (Test Packages) pakkaus wad.service, ja luo sinne luokka AirportServiceTest.

Lisää luokalle tarvittavat annotaatiot sekä oliomuuttujat, ja toteuta luokalle testimetodit, joiden avulla testataan että haluttu lentokone todellakin lisätään lentokentälle. Haluat ainakin tietää että:

  • Kun lentokone on lisätty lentokentälle, tietokannasta samalla tunnuksella haettavalla lentokoneella on asetettu lentokenttä, ja se on juuri se lentokenttä mihin kone on lisätty.
  • Kun lentokone on lisätty lentokentälle, lentokentältä löytyy myös kyseinen kone.
  • Kun lentokone on lisätty yhdelle lentokentälle, se ei ole muilla lentokentillä.
  • Lentokoneen lisääminen samalle lentokentälle useasti ei johda siihen, että lentokenttä sisältää saman koneen monta kertaa.

Aina kun lisäät yksittäisen testin, voit ajaa testit klikkaamalla projektia oikealla hiirennapilla ja valitsemalla "Test".

AircraftControllerTest

Luo testikansioon pakkaus wad.controller ja lisää sinne luokka AircraftControllerTest. Lisää luokkaan tarvittavat määrittelyt, jotta voit käyttää MockMvc-komponenttia testeissä.

Tee seuraavat testit:

  • Kun osoitteeseen /aircrafts tehdään GET-pyyntö, vastauksen status on 200 (ok) ja vastauksen model-oliossa on parametrit aircrafts ja airports.
  • Kun osoitteeseen /aircrafts tehdään POST-pyyntö, jonka parametriksi annetaan name-kenttä, jonka arvona on "HA-LOL", pyynnön vastaukseksi tulee uudelleenohjaus. Tee tämän jälkeen erillinen kysely tietokantaan esim. AircraftRepository:n avulla, ja varmista, että tietokannasta löytyy lentokone, jonka nimi on HA-LOL.
  • Kun osoitteeseen /aircrafts tehdään POST-pyyntö, jonka parametriksi annetaan name-kenttä, jonka arvona on "XP-55", pyynnön vastaukseksi tulee uudelleenohjaus. Tee tämän jälkeen GET-pyyntö osoitteeseen /aircrafts, ja tarkista että pyynnön vastauksena saatavan model-olion sisältämässä "aircrafts"-listassa on juuri luotu lentokone.

Tässäkin tehtävässä, aina kun lisäät yksittäisen testin, voit ajaa testit klikkaamalla projektia oikealla hiirennapilla ja valitsemalla "Test".

FluentLenium

MockMvc:n lisäksi järjestelmätestaukseen käytetään melko paljon käyttöliittymän testaamiseen tarkoitettua Seleniumia ja siihen liittyviä lisäosia kuten FluentLenium. Käytännössä edellämainitut ovat web-selaimen toimintojen automatisointiin tarkoitettuja välineitä, jotka antavat sovelluskehittäjälle mahdollisuuden käydä läpi sovelluksen käyttöliittymää ohjelmallisesti.

Lisätään FluentLenium-kirjaston vaatimat riippuvuudet, oletetaan että testit kirjoitetaan JUnit-testikirjaston avulla (FluentLenium tarjoaa myös muita vaihtoehtoja).

<dependency>
    <groupId>org.fluentlenium</groupId>
    <artifactId>fluentlenium-core</artifactId>
    <version>3.4.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.fluentlenium</groupId>
    <artifactId>fluentlenium-junit</artifactId>
    <version>3.4.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.fluentlenium</groupId>
    <artifactId>fluentlenium-assertj</artifactId>
    <version>3.4.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>htmlunit-driver</artifactId>
</dependency>

Ajatellaan loppukäyttäjän haluamaa toiminnallisuutta "Käyttäjä voi ilmoittautua oppitunnille". Järjestelmä tarjoaa sivun, jonka ensimmäinen linkki vie ilmoittautumissivulle. Ilmoittautumissivulla tulee olla tietty otsikko -- varmistamme, että olemme oikealla sivulla. Tämän lisäksi ilmoiuttautumissivulla on lomakekenttä, jonka attribuutin id arvo on "name". Jos kentällä on attribuutti id, voidaan se valita kirjoittamalla "#kentannimi". Täytetään kenttään arvo "Bob" ja lähetetään lomake. Tämän jälkeen sivulla tulee olla teksti "Ilmoittautuminen onnistui!".

// importit

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ElokuvatietokantaTest extends FluentTest {

    @LocalServerPort
    private Integer port;

    @Test
    public void canSignUp() {
        goTo("http://localhost:" + port);

        find("a").first().click();
        assertEquals("Ilmoittautuminen", window().title());

        find("#name").fill().with("Bob");
        find("form").first().submit();

        assertTrue(pageSource().contains("Ilmoittautuminen onnistui!"));
    }
// ...

Yllä annotaatio @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) käynnistää palvelimen integraatiotestausta satunnaisessa portissa, joka saadaan muuttujaan port annotaation @LocalServerPort avulla.

Yllä menemme ensin paikalliseen osoitteeseen http://localhost:portti, missä portin numero on satunnaisesti valittu -- surffaamme siis haluttuun osoitteeseen. Haemme tämän jälkeen ensimmäisen linkin, eli a-elementin sivulta, ja klikkaamme sitä. Tämän jälkeen tarkistamme, että sivun otsake on Ilmoittautuminen. Tätä seuraa kentän, jonka id on "name" täyttäminen "Bob"-merkkijonolla, jonka jälkeen lomake lähetetään. Kun lomake on lähetetty, haetaan sivun lähdekoodista tekstiä "Ilmoittautuminen onnistui!". Jos tekstiä ei löydy, testi epäonnistuu.

FluentLenium-kirjastoon liittyvää dokumentaatiota löytyy osoitteesta http://www.fluentlenium.org/, jonka lisäksi googlesta löytyy apua seuraavaan tehtävään.

Muistamme toisesta osiosta myös tehtävän, missä tehtiin sovellus elokuvien ja näyttelijöiden hallintaan. Tässä tehtävässä harjoitellaan hieman järjestelmätestausta FluentLeniumin avulla. Tehtävässä ei ole automaattisia testejä, sillä sinun tehtävänä on toteuttaa ne.

Näyttelijän lisääminen ja poistaminen

Luo testikansioon wad.selenium testiluokka ActorTest, johon asetat Selenium-testaamiseen tarvittavat komponentit.

Toteuta testi, jolla varmistetaan että käyttäjän lisääminen ja poistaminen onnistuu. Testin tulee toimia seuraavasti:

  1. Menee näyttelijäsivulle
  2. Tarkistaa ettei sivulla ole tekstiä "Van Damme"
  3. Etsii kentän jonka id on "name", asettaa kenttään tekstin "Van Damme", ja lähettää lomakkeeseen liittyvän lomakkeen.
  4. Tarkistaa että sivulla on teksti "Van Damme"
  5. Klikkaa "Van Damme"en liittyvää poista-nappia
  6. Tarkistaa että sivulla ei ole tekstiä "Van Damme"

Toteuta seuraavaksi testi, joka tekee seuraavat askeleet:

  1. Menee näyttelijäsivulle
  2. Tarkistaa ettei sivulla ole tekstiä "Van Damme"
  3. Tarkistaa ettei sivulla ole tekstiä "Chuck Norris"
  4. Etsii kentän jonka id on "name", asettaa kenttään tekstin "Chuck Norris", ja lähettää lomakkeeseen liittyvän lomakkeen.
  5. Tarkistaa ettei sivulla ole tekstiä "Van Damme"
  6. Tarkistaa että sivulla on teksti "Chuck Norris"
  7. Etsii kentän jonka id on "name", asettaa kenttään tekstin "Van Damme", ja lähettää lomakkeeseen liittyvän lomakkeen.
  8. Tarkistaa että sivulla on teksti "Van Damme"
  9. Tarkistaa että sivulla on teksti "Chuck Norris"
  10. Klikkaa "Van Damme"en liittyvää poista-nappia
  11. Klikkaa henkilön "Chuck Norris" poista-nappia
  12. Tarkistaa ettei sivulla ole tekstiä "Van Damme"
  13. Tarkistaa eteti sivulla ole tekstiä "Chuck Norris"

Elokuvan lisääminen ja näyttelijän lisääminen elokuvaan

Luo testikansioon wad.selenium testiluokka MovieTest, johon asetat Selenium-testaamiseen tarvittavat komponentit.

Toteuta seuraavat askeleet

  1. Mene elokuvasivulle
  2. Tarkista että sivulla ei ole tekstiä "Bloodsport"
  3. Tarkista että sivulla ei ole tekstiä "Van Damme"
  4. Etsi kenttä jonka id on "name" ja lisää siihen arvo "Bloodsport"
  5. Etsi kenttä jonka id on "lengthInMinutes" ja lisää siihen arvo "92"
  6. Lähetä kenttään liittyvä lomake
  7. Tarkista että sivulla on teksti "Bloodsport"
  8. Tarkista että sivulla ei ole tekstiä "Van Damme"
  9. Mene näyttelijäsivulle
  10. Tarkista ettei sivulla ole tekstiä "Van Damme"
  11. Etsi kenttä jonka id on "name", aseta kenttään teksti "Van Damme", ja lähetä lomake.
  12. Tarkistaa että sivulla on teksti "Van Damme"
  13. Etsi linkki, jossa on teksti "Van Damme" ja klikkaa siitä.
  14. Etsi nappi, jonka id on "add-to-movie", ja klikkaa sitä.
  15. Mene elokuvasivulle
  16. Tarkista että sivulla on teksti "Bloodsport"
  17. Tarkista että sivulla on teksti "Van Damme"

Suorita taas testit klikkaamalla projektia oikealla hiirennäppäimellä ja valitsemalla Test.

Konfiguraatioprofiilit ja testaaminen

Testien ajamisessa voidaan käyttää myös konfiguraatioprofiileja. Kun sovellukselle on määritelty erilaisia profiileja, esimerkiksi kirjautumiseen liittyvät konfiguraatiot, voidaan tietty profiili aktivoida testeissä. Testin aktivointi tapahtuu annotaation ActiveProfiles avulla.

Alla olevassa esimerkissä testiluokan testit suoritetaan siten, että käytössä on profiiliin "test" liittyvä konfiguraatio, eli se konfiguraatio, joka on määritelty annotaatiolla @Profile("test") (tai @Profile(values = {"test", "muita"}) jos halutaan että samaa konfiguraatiota käytetään useammassa profiilissa.

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("dev")
public class ApplicationTest {
    // ...

Tyypillinen ohjelmistokehitysprosessi

Ohjelmiston elinkaareen kuuluu vaatimusmäärittely, suunnittelu, toteutus, testaus, sekä ylläpito ja jatkokehitys. Vaatimusmäärittelyyn kuuluu ohjelmistoon liittyvien toiveiden ja vaatimusten kartoitus, jota seuraa suunnittelu, missä pohditaan miten vaatimukset toteutetaan. Toteutusvaihe sisältää ohjelmointia sekä sovelluksen elinympäristöön liittyvien komponenttien yhteensovittamista. Testaukseen kuuluu sovelluksen testaus niin automaattisesti kuin manuaalisesti. Kun ohjelmisto tai sen osa on toiminnassa, tulee elinkaaren osaksi myös käytössä olevasta ohjelmistosta löytyvien virheiden korjaaminen sekä uusien ominaisuuksien kehittäminen.

Ohjelmointiin ja ohjelmistojen kehitykseen liittyy jatkuva etsiminen ja kokeileminen. Ongelmat pyritään ratkaisemaan kokeilemalla vaihtoehtoja kunnes ongelmaan löytyy sopiva ratkaisu. Jos ongelma on osittain tuttu, on tarkasteltavia vaihtoehtoja vähemmän, ja jos ongelma on tuttu, on siihen tyypillisesti ainakin yksi valmis ratkaisumalli. Tämän hetken suosituimmat ohjelmistokehitysmenetelmät (ketterät menetelmät kuten Scrum ja Kanban) ohjaavat työn läpinäkyvyyteen, oman työskentelyn kehittämiseen sekä siihen, että muiden osallistuminen ohjelmistokehitykseen on helppoa.

Ohjelmistoon liittyvät toiveet ja vaatimukset

Ohjelmistoon liittyvistä toiveista ja vaatimuksista keskustellaan asiakkaan ja käyttäjien kanssa, ja ne kirjataan muistiin. Vaatimukset kirjataan usein lyhyessä tarinamuodossa, joka kerrotaan uutta toiminnallisuutta toivovan henkilön näkökulmasta: "As a (käyttäjän tyyppi) I want (tavoite) so that (syy)." -- esimerkiksi "As a user I want to be able to view the messages so that I can see what others have written". Vaatimuksia kirjattaessa saadaan kuva ohjelmistolta toivotusta toiminnallisuudesta, jonka jälkeen toiminnallisuuksia voidaan järjestää tärkeysjärjestykseen.

Toiminnallisuuksien tärkeysjärjestykseen asettaminen tapahtuu yhdessä asiakkaan ja käyttäjien kanssa. Kun toiminnallisuudet ovat kutakuinkin tärkeysjärjestyksessä, valitaan niistä muutama kerrallaan työstettäväksi. Samalla varmistetaan asiakkaan kanssa, että ohjelmistokehittäjät ja asiakas ymmärtävät toiveen samalla tavalla. Kun toiminnallisuus on valmis, toiminnallisuus näytetään asiakkaalle ja asiakas pääsee kertomaan uusia toiminnallisuustoiveita sekä mahdollisesti uudelleenjärjestelemään vaatimusten tärkeysjärjestystä.

Vaatimuksia ja toiveita, sekä niiden kulkemista projektin eri vaiheissa voidaan käsitellä esimerkiksi Trello:n avulla. Ohje Trellon käyttöön.

Versionhallinta

Ohjelmiston lähdekoodin ja dokumentaatio tallennetaan keskitetysti versionhallintaan, mistä kuka tahansa voi hakea ohjelmistosta uusimman version sekä lähettää sinne uudemman päivitetyn version. Käytännössä jokaisella ohjelmistokehittäjällä on oma hiekkalaatikko, jossa ohjelmistoon voi tehdä muutoksia vaikuttamatta muiden tekemään työhön. Jokaisella ohjelmistokehittäjällä on yleensä samat tai samankaltaiset työkalut (ohjelmointiympäristö, ...), mikä helpottaa muiden kehittäjien auttamista.

Kun ohjelmistokehittäjä valitsee vaatimuksen työstettäväksi, hän tyypillisesti hakee projektin versionhallinnasta projektin uusimman version, sekä lähtee toteuttamaan uutta vaatimusta. Kun vaatimukseen liittyvä osa tai komponentti on valmis sekä testattu paikallisesti (automaattiset testit on olemassa, toimii ohjelmistokehittäjän koneella), lähetetään uusi versio versionhallintapalvelimelle.

Versionhallintapalvelin sisältää myös mahdollisesti useampia versioita projektista. Esimerkiksi git-mahdollistaa ns. branchien käyttämisen, jolloin uusia ominaisuuksia voidaan toteuttaa erillään "päähaarasta". Kun uusi ominaisuus on valmis, voidaan se lisätä päähaaraan. Versionhallinnassa olevia koodeja voidaan myös tägätä julkaisuversioiksi.

Yleisin versionhallintatyökalu on Git, joka on käytössä Githubissa. Ensiaskeleet Githubin käyttöön.

Jatkuva integraatio

Versionhallintapalvelin on tyypillisesti kytketty integraatiopalvelimeen, jonka tehtävänä on suorittaa ohjelmistoon liittyvät testit jokaisen muutoksen yhteydessä sekä tuottaa niistä mahdollisesti erilaisia raportteja. Integraatiopalvelin kuuntelee käytännössä versionhallintajärjestelmässä tapahtuvia muutoksia, ja hakee uusimman lähdekoodiversion muutoksen yhteydessä.

Kun testit ajetaan sekä paikallisella kehityskoneella että erillisellä integraatiokoneella ohjelmistosta huomataan virheitä, jotka eivät tule esille muutoksen tehneen kehittäjän paikallisella koneella (esimerkiksi erilainen käyttöjärjestelmä, selain, ...). On myös mahdollista että ohjelmistosta ei noudeta kaikkia sen osia -- ohjelmisto voi koostua useista komponenteista -- jolloin kaikkien vaikutusten testaaminen paikallisesti on mahdotonta. Jos testit eivät mene läpi integraatiokoneella, korjataan muutokset mahdollisimman nopeasti.

Työkaluja automaattiseen kääntämiseen ja jatkuvaan integrointiin ovat esimerkiksi Travis ja Coveralls. Travis varmistaa että viimeisin lähdekoodiversio kääntyy ja että testit menevät läpi, ja Coveralls tarjoaa välineitä testikattavuuden ja projektin historian tarkasteluun -- tässä hyödyksi on esimerkiksi Cobertura. Kummatkin ovat ilmaisia käyttää kun projektin lähdekoodi on avointa -- kumpikin tarjoaa myös suoran Github-tuen.

Travisin käyttöönottoon vaaditaan käytännössä se, että projekti on esimerkiksi Githubissa ja että sen juurikansiossa on travisin konfiguraatiotiedosto .travis.yml. Yksinkertaisimmillaan konfiguraatiotiedosto sisältää vain käytetyn ohjelmointikielen -- travis osaa esimerkiksi päätellä projektin tyypin pom.xml-tiedoston pohjalta. Ohje Traviksen käyttöönottoon.

Nopeasti näytille

Kun uusi vaatimus tai sen osa on saatu valmiiksi, kannattaa viedä palvelimelle palautteen saamista varten. On tyypillistä, että ohjelmistolle on ainakin Staging- ja Tuotanto-palvelimet. Staging-palvelin on lähes identtinen ympäristö tuotantoympäristöön verrattuna. Staging (usein myös QA)-ympäristöön kopioidaan ajoittain tuotantoympäristön data, ja se toimii viimeisenä testaus- ja validointipaikkana (Quality assurance) ennen tuotantoon siirtoa. QA-ympäristöä käytetään myös demo- ja harjoitteluympäristönä. Kun QA-ympäristössä oleva sovellus on päätetty toimivaksi, siirretään sovellus tuotantoympäristöön.

Tuotantoympäristö voi olla yksittäinen palvelin, tai se saattaa olla joukko palvelimia, joihin uusin muutos viedään hiljalleen. Tuotantoympäristö on tyypillisesti erillään muista ympäristöistä mahdollisten virheiden minimoimiseksi.

Käytännössä versioiden päivitys tuotantoon tapahtuu usein automaattisesti. Esimerkiksi ohjelmistoon liittyvä Travis-konfiguraatio voidaan määritellä niin, että jos kaikki testit menevät läpi integraatiopalvelimella, siirretään ohjelmisto automaattisesti tuotantoon. Esimerkiksi Herokussa sijaitsevaan sovellukseen muutokset voidaan hakea automaattisesti Githubista (ohje).

Kannattaa vielä lukea aiheesta blogikirjoitus.

Tehtäväpohjassa on yksinkertainen sovellus, joka mahdollistaa esineiden lisäämisen ja listaamisen. Tässä tehtävässä harjoittelet sovelluksen siirtämistä Githubiin, Travisiin ja Herokuun. Joutunet tekemään aika selvittelyä tehtävää ratkoessa.

Github

Jos käytössäsi ei ole Github tunnusta, luo se nyt. Lisää tämän jälkeen tehtävän koodit githubiin (huom! älä siirrä target-kansiota!).

Kun sovelluksen lähdekoodit ovat oman tunnuksesi alla Githubissa, muokkaa luokan AliveApplication metodia public static String githubUrl siten, että se palauttaa sovelluksen Github-osoitteen.

Travis

Luo tämän jälkeen Travis-tunnus ja lisää projekti Travis-palveluun. Traviksen tulee ajaa sovelluksen testit aina kun sovelluksen uusi versio lisätään Githubiin.

Kun github-travis -integraatio toimii, muokkaa luokan AliveApplication metodia public static String travisUrl siten, että se palauttaa sovelluksen Travis-osoitteen.

Heroku

Luo lopulta Heroku-tunnus ja vie sovellus Herokuun. Sovelluksen tulee olla kytkettynä Travis-palveluun. Jos sovelluksen testit menevät läpi Travis-palvelussa kun uusi versio lisätään Githubiin, tulee sovelluksen uusi versio viedä myös Herokuun.

Kun github-travis-heroku -integraatio toimii, muokkaa luokan AliveApplication metodia public static String herokuUrl siten, että se palauttaa sovelluksen Heroku-osoitteen.

Sisällysluettelo