เลิก manual test แล้วมาเขียน unit test Java Project ด้วย Spock กันเถอะ

unit test คือการเขียนคำสั่งไปทดสอบ method หรือ function ที่เราสร้างขึ้น ว่าทำงานได้ถูกต้องหรือไม่ แทนที่จะต้องรอระบบให้เรียบร้อย แล้วมา manual test ทีหลัง แต่เราสามารถแยกส่วนของระบบเป็นหน่วยย่อย ๆ แยกกัน test ได้

unit test ได้รับการยอมรับว่าเป็น software practice ที่ดี ลดความผิดพลาดของ program ที่เราสร้างขึ้น ช่วยให้ค้นพบข้อมูลผิดพลาดไวขึ้น แก้ปัญหาได้ไวขึ้น ชีวิตง่ายขึ้น มีความสุขมากขึ้น

unit test ทำให้เรามีความมั่นใจ เมื่อแก้ไขส่วนต่างๆ ของ program แล้ว run unit test ก็สามารถรู้ได้ทันทีว่า สิ่งที่เราแก้ไขนั้น กระทบส่วนไหนในระบบบ้าง

ในวันนี้ เราจะได้มาเรียนรู้การใช้งาน unit test framework ตัวที่ชื่อว่า Spock

  • Spock เป็น test framework เช่นเดียวกับ JUnit, PHPUnit, NUnit, RSpec
  • ถูกสร้างขึ้นเพื่อ test Java Project และในการเขียน test
  • สามารถใช้เขียน test Java SE, Java Web, Android ก็ได้
  • ใช้ภาษา Groovy ที่สั้น กระชับ ลดขั้นตอนต่างๆ มีความ modern
  • Spock ถูกสร้างโดยให้มีลักษณะเช่นเดียวกับ Behavior Driven Development (BDD) ทำให้ test case สามารถอ่านได้ง่าย
  • พัฒนาต่อยอดจาก JUnit ทำให้สามารถใช้ test runner ของ JUnit และ Report ของ JUnit ได้เลย

ลองมาดู test method ที่สร้างด้วย Spock กัน

def "score 90 get grade A"() {

   given:
   def gradeCalculator = new GradeCalculator()
   def rawScore = 90

   when:
   def result = gradeCalculator.getGradeForScore(rawScore)

   then:
   result == "A"
}

โดยทั่วไปโครงสร้างการเขียน test method นั้นเราจะเขียนในรูปแบบ AAA ย่อมาจาก arrange, act, assert หมายความว่า ในแต่ละ test method จะมีขั้นตอนทำงานหลักๆ แบ่งเป็น 3 ส่วน

  • arrange : เตรียมข้อมูล
  • act : ใช้งาน method ที่เราต้องการ test
  • assert : เปรียบเทียบผลลัพธ์การทำงานกับผลลัพธ์ที่เราต้องการ ว่าตรงกันหรือไม่ ส่วนนี้เราคาดหวังที่จะได้ true กลับมา เพื่อทำให้ test นี้ผ่าน

เมื่อกลับไปดูที่ Spock test method จะเห็นได้ว่าโครงสร้างเป็น given: , when : , then : ถ้าเปรียบกับ AAA pattern จะเป็นดังนี้

  • given ก็คือส่วน arrange
  • when คือ action
  • then คือ assert

!!! และที่สำคัญ เราสามารถเขียน test method เป็นชุด string เป็นประโยค อ่านเข้าใจง่ายได้เลย ซึ่งไม่มีใน test framework ตัวอื่นๆ ของ Java ตรงนี้ เป็นความสามารถพิเศษที่มาจาก Groovy Magic DSL ดังเช่นในตัวอย่างนี้ def "score 90 must have grade A"()

เราจะเรียนรู้ Spock ผ่านการสร้าง project เพื่อทดสอบ Java class

เริ่มต้นสร้าง Gradle project ใหม่

โดยกำหนดค่า

  • group id : com.codesanook.example
  • artifect id : beginning-spock

image 2

image 3

image 4

image 5

แก้ไข gradle file เพื่อ include library ที่จำเป็น เลือก Spock version ให้ตรงกับ Groovy Version ด้วยนะครับ

group 'com.codesanook.example'
version '1.0-SNAPSHOT'

apply plugin: 'java'

//เพิ่ม plugin groovy เข้าไป
apply plugin: 'groovy'

sourceCompatibility = 1.7

repositories {
   mavenCentral()
}

dependencies {

//เพิ่ม lib ที่ต้องใช้ใน testCompile คือ groovy และ spock ดู version ให้ตรงกันด้วยนะครับ
   testCompile 'org.codehaus.groovy:groovy-all:2.4.4'
   testCompile "org.spockframework:spock-core:1.0-groovy-2.4"
}

สร้าง Java class สำหรับคำนวณเกรด

เราจะสร้าง class GradeRate และ GradeCalcualtor ไว้คำนวณ Grade ผลการเรียนครับ แล้วจะใช้ Spock มาทดสอบ method ต่างๆ ของ class GradeCalcualtor

สร้าง Java Class GradeRate.java

src\main\java\com\codesanook\example\GradeRate.java

package com.codesanook.example;

public class GradeRate implements Comparable<GradeRate> {

   private float minScore;
   private String grade;

   public GradeRate(float minScore, String grade) {
       this.minScore = minScore;
       this.grade = grade;
   }


   public float getMinScore() {
       return minScore;
   }

   public void setMinScore(float minScore) {
       this.minScore = minScore;
   }


   public String getGrade() {
       return grade;
   }

   public void setGrade(String grade) {
       this.grade = grade;
   }

   @Override
   public int compareTo(GradeRate other) {
       return Float.compare(this.getMinScore(), other.getMinScore());
   }


}

src\main\java\com\codesanook\example\GradeCalculator.java

package com.codesanook.example;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class GradeCalculator {

   private List<GradeRate> rates;
   private int currentIndex;

   public GradeCalculator() {
       rates = new ArrayList<>();
       rates.add(new GradeRate(85, "A"));
       rates.add(new GradeRate(70, "B"));
       rates.add(new GradeRate(60, "C"));
       rates.add(new GradeRate(50, "D"));
       rates.add(new GradeRate(0, "F"));


       Collections.sort(rates);
       resetIndex();
   }

   private void resetIndex() {
       currentIndex = rates.size() - 1;
   }

   public String getGradeForScore(double rawScore) {
       if (rawScore > 100 || rawScore < 0)
           throw new IllegalStateException("Score must be between 0 to 100");

       if (rawScore >= rates.get(currentIndex).getMinScore()) {
           String grade = rates.get(currentIndex).getGrade();
           resetIndex();
           return grade;
       }

       currentIndex--;
       return getGradeForScore(rawScore);
   }
}

สร้าง Test Class กัน ใน Spock Test Class เราเรียกว่า Specification โดย class ที่สร้างขึ้น จะสืบทอดจาก Specification และนิยมตั้งชื่อ class ให้ลงท้ายด้วย Spec

สร้าง Groovy class GradeCalculatorSpec.groovy

src\test\groovy\com\codesanook\example\test\GradeCalculatorSpec.groovy

package com.codesanook.example.test

import com.codesanook.example.GradeCalculator
import spock.lang.Specification
import spock.lang.Unroll

class GradeCalculatorSpec extends Specification {

   def "score 90 get grade A"() {

       given:
       def gradeCalculator = new GradeCalculator()
       def rawStore = 90

       when:
       def result = gradeCalculator.getGradeForScore(rawStore)

       then:
       result == "A"
   }
}

ทดสอบการทำงาน

เปิด Gradle Window double click run หรือสั่งจาก command line gradle run

หากการทำงานถูกต้อง จะมี test report file ให้เราเข้าไปดูได้ อยู่ใน build folder

build\reports\tests\index.html

image 1

แถมให้อีกนิด

ถ้าเราต้องการเขียน test method ขึ้นมาเพื่อทดสอบว่า ถ้ามีคะแนน 60, 70, 85 บ้าง เด็กนักเรียนคนนี้ควรได้ grade อะไร หากต้องสร้าง method ใหม่อีก 3 method ก็คงเป็นเรื่องน่าเบื่อ และเรายังต้องเขียน code ซ้ำกัน ไม่ตรงตาม concept codesanook อีก ดังนั้น เราสามารถยุบให้เหลือ method เดียวกันได้ด้วย where ครับ

เราสามารถเพิ่ม method นี้เข้าไปใน class GradeCalculatorSpec.groovy ได้เลย

     @Unroll
    def "score #rawScore get grade #grade"() {

        given:
        def gradeCalculator = new GradeCalculator()

        when:
        def result = gradeCalculator.getGradeForScore(rawScore)

        then:

        result == grade

        where:
        rawScore | grade
        85       |  "A"
        70       |  "B"
        60       |  "C"
        50       |  "D"
        49       |  "F"
    }
  • @Unroll ช่วยให้ผลการ test ของแต่ละ where case แยกออกจากกัน
  • เราสามารถ กำหนดค่าตัวแปรของแต่ละ case เข้าไปใน method name ได้ ด้วย #variable

ทดลอง run test อีกครั้ง เราจะเห็นได้ว่า มีผลลัพธ์ของ test เพิ่มขึ้นมาอีก 5 tests

image 1

จะเห็นได้ว่า Spock เป็น test framework ตัวนึง ที่น่าใช้งานไม่น้อย และเข้ากันได้ดีกับ JUnit ถ้าผู้ใช้มีความพื้นฐานอยู่แล้ว ก็สามารถต่อยอดได้ไว อีกทั้งยังนำส่วนดีของ Groovy มาใช้ได้ด้วย สามารถทดสอบ production code ที่เป็น Java ได้ เรียกว่านำส่วนดีของหลายๆ อย่างมารวมกัน

ถ้าใครมีคำถาม ข้อสงสัยใดๆ comment มาได้เลยนะครับ

download source code