Pengujian Aplikasi Griffon Secara Otomatis Melalui VirtualBox


Salah satu istilah yang diperkenalkan oleh metode pengembangan Extreme Programming (XP) adalah continuous integration, continuous delivery, dan continuous deployment. Penggunaan kata continuous disini menunjukkan semangat XP dimana hasil harus bisa diperoleh secepat mungkin.

Continuous integration (CI) adalah pengujian yang dilakukan secara otomatis setiap kali developer men-push perubahan yang mereka lakukan. Karena CI dilakukan secara otomatis atau berkala, perubahan kode program yang menyebabkan aplikasi tidak dapat berjalan akan diketahui secepat mungkin.

Continous delivery (CD) memastikan bahwa kode program yang telah dibuat dapat segera dijalankan di production bila dibutuhkan. Dengan adanya CD, bila klien meminta demo aplikasi terbaru atau tampilan web terbaru secara tiba-tiba pada hari itu juga, tidak akan ada yang panik.

Continous deployment adalah continous delivery yang direalisasikan secara berkala dan otomatis ke production, misalnya pengguna memperoleh update terbaru setiap hari atau halaman web diperbaharui setiap minggu. Continuous delivery hanya menyatakan sebuah kesiapan tapi continuous deployment adalah realisasi dari continuous delivery secara rutin.

Kata kunci dari semua yang berbau continuous disini adalah otomatis, terstruktur, dan berkala. Oleh sebab itu, ada banyak tools pihak ketiga seperti Travis CI, Jenkins CI, Vagrant, Packer, Chef dan sebagainya yang dapat dipakai untuk mewujudkan berbagai continuous tersebut.

Salah satu fasilitas menarik yang saya temui dari Travis CI adalah kemampuan untuk menguji kode program di beberapa virtual machine berbeda (misalnya versi Java berbeda atau sistem operasi berbeda). Travis kemudian akan melaporkan hasil pengujian dalam bentuk matrix. Jenkins CI juga memungkinkan hal serupa dengan memakai plugin Vagrant. Vagrant adalah sebuah tools untuk mengelola virtual machine (seperti menyalakan, mematikan, memberi perintah, dan sebagainya) secara standar dan dilengkapi dukungan VirtualBox secara bawaan. Selain itu, Vagrant juga memiliki banyak image virtual machine siap pakai di Vagrant Cloud (https://vagrantcloud.com).

Pada artikel ini, saya tidak akan menerapkan semua jenis continuous pada XP (dan sesungguhnya saya bukan fans XP🙂 ). Saya juga tidak akan memakai Vagrant. Saya hanya ingin menjalankan pengujian aplikasi Griffon di VirtualBox secara otomatis. Untuk itu, saya menyiapkan 2 image yang mewakili Windows 7 dan Linux Mint. VirtualBox dapat dikendalikan dengan menggunakan COM API. Namun, ia juga menyediakan beberapa front-end lain yang lebih mudah dipakai seperti web services (SOAP) dan perintah VBoxManage. Pada kesempatan ini, saya akan mencoba mengendalikan VirtualBox dengan menggunakan VBoxManage yang dipanggil melalui CLI.

Langkah pertama yang saya lakukan adalah membuat sebuah perintah baru yang dapat dipakai di aplikasi Griffon dengan memberikan perintah berikut ini:

C:\proyek> griffon create-script vbox-test

Perintah dalam Griffon adalah sebuah Gant script. Mungkin ini tidak akan berlaku lagi setelah Griffon 2 yang mendukung Gradle dirilis. Saya akan menemukan sebuah file baru bernama VboxTest.groovy di folder scripts. Saya kemudian mengubah isi script tersebut menjadi seperti berikut ini:

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

def hasil = [:]

def tambahGagal = { String vBoxImage, def jumlahGagal ->
    if (!hasil.containsKey(vBoxImage)) {
        hasil[vBoxImage] = jumlahGagal
    } else {
        hasil[vBoxImage] = hasil[vBoxImage] + jumlahGagal
    }
}

def exec = { String command, String vBoxImage, boolean silentFail, Object... args ->
    List commands = [buildConfig.vbox.vboxManage, command, vBoxImage]
    args?.each { arg ->
        if (arg instanceof Map) {
            arg.each { k, v ->
                commands << ("--" + k)
                commands << v
            }
        } else {
            commands << arg as String
        }
    }
    ProcessBuilder builder = new ProcessBuilder(commands)
    builder.redirectErrorStream(true)
    if (!silentFail) {
        println "Mengerjakan ${commands}"
    }
    Process p = builder.start()
    if (silentFail) {
        p.waitForOrKill(10000000)
    } else {
        p.waitForProcessOutput(System.out, System.out)
        if (p.exitValue() != 0) {
            throw new RuntimeException("Gagal mengerjakan perintah. Exit code: ${p.exitValue()}")
        }
    }
    p.exitValue()
}

def execute = { String vBoxImage, String image, def config, def args = [] ->
    def p = [image: image, username: config.username]
    if (config.containsKey('password')) p.password = config.password
    exec('guestcontrol', vBoxImage, false, 'execute', p, '--wait-exit', '--wait-stdout', '--wait-stderr', *args)
}

def executeSilent = { String vBoxImage, String image, def config, def args = [] ->
    def p = [image: image, username: config.username]
    if (config.containsKey('password')) p.password = config.password
    exec('guestcontrol', vBoxImage, true, 'execute', p, '--wait-exit', '--wait-stdout', '--wait-stderr', *args)
}

def copyTo = { String vBoxImage, def source, def dest, def config ->
    def p = ['username': config.username]
    if (config.containsKey('password')) p.password = config.password
    String strSource = source instanceof Path? source.toAbsolutePath().toString(): source
    String strDest = dest instanceof Path? dest.toAbsolutePath().toString(): dest
    exec('guestcontrol', vBoxImage, false, 'copyto', strSource, strDest, p)
}

def copyFrom = { String vBoxImage, def source, def dest, def config ->
    def p = ['username': config.username]
    if (config.containsKey('password')) p.password = config.password
    String strSource = source instanceof Path? source.toAbsolutePath().toString(): source
    String strDest = dest instanceof Path? dest.toAbsolutePath().toString(): dest
    exec('guestcontrol', vBoxImage, false, 'copyFrom', strSource, strDest, p)
}

def snapshotRestore = { String vBoxImage, def config ->
    exec('controlvm', vBoxImage, false, 'poweroff')
    exec('snapshot', vBoxImage, false, 'restorecurrent')
}

def concat = { String path1, String path2, String separator ->
    if (path1.endsWith(separator)) {
        return path1 + path2
    } else {
        return path1 + separator + path2
    }
}

def proses = {String vBoxImage, def config  ->
    def separator = (config.os == 'windows')? '\': '/'

    exec('startvm', vBoxImage, false, [type: 'headless'])

    // menunggu sistem operasi guest siap
    print "Menunggu virtual machine siap  "
    int exitCode = -1
    while (exitCode != 0) {
        exitCode = executeSilent(vBoxImage, config.testAliveCmd, config)
        print "."
        sleep(5000)
    }
    println ""
    println "Virtual machine sudah siap"

    // membuat file ZIP untuk di-copy ke host
    println "Membuat file ZIP untuk dipindahkan ke guest"
    def baseDir = griffonSettings.baseDir.path
    Path tempZip = Files.createTempDirectory("griffonvboxtemp").resolve('project.zip')
    ant.zip(destFile: tempZip.toAbsolutePath().toString(), basedir: baseDir)
    tempZip.toFile().deleteOnExit()

    // mencopy file project ke host
    println "Mencopy file ZIP ke guest"
    if (config.os == 'windows') {
        copyTo(vBoxImage, tempZip, config.targetDir, config)
    } else {
        copyTo(vBoxImage, tempZip, config.targetDir + separator + 'project.zip', config)
    }

    // men-extract file ZIP di host
    println "Men-extract file ZIP di guest"
    def targetZip = concat(config.targetDir, 'project.zip', separator)
    def targetExtractZip = concat(config.targetDir, 'project', separator)
    execute(vBoxImage, config.sevenZip, config, ['--', 'x', targetZip, '-y', '-o' + targetExtractZip])

    // mengerjakan test-app
    println "Mengerjakan test-app"
    try {
        def envs = ""GRIFFON_HOME=${config.griffonHome} JAVA_HOME=${config.javaHome}"".toString()
        execute(vBoxImage, config.griffonExec, config, ['--environment', envs, '--',
                                                        '-Dbase.dir=' + targetExtractZip, 'test-app'])
    } catch (RuntimeException ex) {
        println "PENTING: Pengujian untuk proyek ini gagal!"
        tambahGagal(vBoxImage, 1)
    }

    // mengambil hasil pengujian di guest di folder targettest-reportsTESTS-TestSuites.xml
    println "Men-copy file hasil pengujian dari guest"
    def lokasiTestReports = Paths.get(baseDir, 'target', 'vbox-test-reports')
    if (!lokasiTestReports.toFile().exists()) lokasiTestReports.toFile().mkdirs()
    def dest = lokasiTestReports.resolve("TESTS-$vBoxImage-TestSuites-${new Date().format('yyyyMMddhhmm')}.xml")
    def source = concat(targetExtractZip, 'target' + separator + 'test-reports' + separator + 'TESTS-TestSuites.xml', separator)
    copyFrom(vBoxImage, source, dest, config)
    println "File $dest berhasil dibuat"

    // membaca hasil pengujian
    def testsuites = new XmlSlurper().parse(dest.toFile())
    testsuites.testsuite.each { node ->
        if (node.@errors.toInteger() > 0) {
            tambahGagal(vBoxImage, node.@errors.toInteger())
        }
        if (node.@failures.toInteger() > 0) {
            tambahGagal(vBoxImage, node.@failures.toInteger())
        }
    }

}

target(name: 'vboxtest', description: "Run test in VirtualBox", prehook: null, posthook: null) {

    buildConfig.vbox.images.each { k, v ->
        proses(k,v)

        // rollback ke state terakhir
        println "Mengembalikan snapshot seperti semula"
        snapshotRestore(k, v)
    }

    println "nHasil pengujian:"
    println '-'*40
    printf "%-30s %5sn", 'VM Image', 'Gagal'
    println '-'*40
    hasil.each { k,v ->
        printf "%-30s %5dn", k, v
    }
    println '-'*40
    println "nSelesai.n"

}

setDefaultTarget('vboxtest')

Pada script yang saya buat di atas, saya memiliki asumsi bahwa Java, Griffon dan database yang dibutuhkan telah ter-install dimasing-masing image. Saya perlu mendaftarkan setiap pengaturan yang berkaitan dengan masing-masing image dengan menuliskannya di file BuildConfig.groovy. Sebagai contoh, saya menambahkan baris berikut ini pada BuildConfig.groovy:

vbox {

    vboxManage = 'C:\\Program Files\\Oracle\\VirtualBox\\VBoxManage.exe'

    images {
        'Windows 7 Test' {
            os = 'windows'
            username = 'Tester'
            testAliveCmd = 'C:\\Windows\\System32\\ipconfig.exe'
            targetDir = 'C:\\Users\\Tester\\Desktop\\'
            sevenZip = 'C:\\Progra~1\\7-Zip\\7z.exe'
            griffonExec = 'C:\\Progra~1\\Griffon-1.5.0\\bin\\griffon.bat'
            griffonHome = 'C:\\Progra~1\\Griffon-1.5.0'
            javaHome = 'C:\\Progra~1\\Java\\jdk1.7.0_21'
        }
        'Linux Mint Test' {
            os = 'linux'
            username = 'tester'
            password = 'toor'
            testAliveCmd = '/bin/ls'
            targetDir = '/home/tester/Desktop'
            sevenZip = '/usr/bin/7z'
            griffonExec = '/home/tester/griffon-1.5.0/bin/griffon'
            griffonHome = '/home/tester/griffon-1.5.0'
            javaHome = '/usr/lib/jvm/java-7-openjdk-i386'
        }
    }
}

Konfigurasi di atas juga memungkinkan sebuah image yang sama dijalankan untuk diuji lebih dari sekali, misalnya dengan nilai griffonHome dan/atau javaHome berbeda.

Virtual machine akan dijalankan dalam modus headless dimana tidak ada layar UI sehingga pengguna tidak bisa berinteraksi dengannya secara langsung. Untuk mengendalikan virtual machine yang berada dalam keadaan headless seperti ini, saya akan memanggil VBoxManage.exe pada script yang saya buat.

Perintah guestcontrol pada VBoxManage.exe sangat bergantung pada Guest Additions. Salah satu penyebab pesan kesalahan random seperti Session is not in started state adalah versi Guest Additions yang sudah kadaluarsa. Untuk men-install versi Guest Additions terbaru, saya dapat memilih menu Devices, Install Guest Additions CD image…. Versi Guest Additions bisa berbeda karena saat saya men-upgrade VirtualBox ke versi terbaru, masing-masing image tetap memakai Guest Additions versi lama (yang harus di-upgrade secara manual).

Saya tidak bisa langsung memberikan perintah pada virtual machine yang baru dinyalakan. Oleh sebab itu, terdapat nilai testAliveCmd di konfigurasi yang berisi sebuah perintah yang dapat dipakai untuk menguji apakah proses startup sistem operasi virtual sudah selesai atau belum. Bila perintah ini sukses dikerjakan, maka saya bisa segera lanjut mengirim file zip berisi kode program proyek ke virtual machine dan meng-extract file tersebut di dalam virtual machine. Saya mengandaikan bahwa 7zip telah ter-install di guest. 7zip biasanya sudah ada dalam distro Linux populer, tetapi harus di-install terpisah di Windows.

Setelah itu, di dalam virtual machine, saya mengerjakan perintah test-app dari Griffon untuk menguji aplikasi. Setelah pengujian selesai, saya mengambil file XML yang dihasilkan dan meletakkannya di folder target/vbox-test-reports milik proyek di host (proyek yang menjalankan pengujian bukan yang diuji). Tidak lupa saya memberikan perintah agar virtual machine dikembalikan seperti pada snapsnot semula. Disini saya mengasumsikan bahwa virtual machine telah diberi snapshot sebelumnya. Dengan demikian, setiap kali saya menjalankan pengujian, sistem operasi virtual akan selalu berada dalam kondisi yang sama. Ini adalah salah satu kelebihan melakukan pengujian pada virtual machine.

Untuk menjalankan script di atas, saya akan memberikan perintah berikut ini:

C:\proyek> griffon vbox-test
...
Waiting for VM "Windows 7 Test" to power on...
VM "Windows 7 Test" has been successfully started.
Menunggu virtual machine siap  .
Virtual machine sudah siap
Membuat file ZIP untuk dipindahkan ke guest
...
Mencopy file ZIP ke guest
...
Men-extract file ZIP di guest
...
Mengerjakan test-app
...
Men-copy file hasil pengujian dari guest
...
Mengembalikan snapshot seperti semula
...
Waiting for VM "Linux Mint Test" to power on...
VM "Linux Mint Test" has been successfully started.
Menunggu virtual machine siap  .
Virtual machine sudah siap
Membuat file ZIP untuk dipindahkan ke guest
...
Mencopy file ZIP ke guest
...
Men-extract file ZIP di guest
...
Mengerjakan test-app
...
Men-copy file hasil pengujian dari guest
...
Mengembalikan snapshot seperti semula
...

Hasil pengujian:
----------------------------------------
VM Image                       Gagal
----------------------------------------
Windows 7 Test                    12
Linux Mint Test                   12
----------------------------------------

Selesai.

Perintah di atas menunjukkan ada 12 pengujian yang gagal, baik di Windows mapun di Linux. Untuk mendapatkan informasi lebih lanjut, saya dapat membaca file XML hasil pengujian yang terletak di folder target/vbox-test-reports seperti yang terlihat pada gambar berikut ini:

Informasi hasil pengujian di setiap image

Informasi hasil pengujian di setiap image

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: