Matchers 1.1

หลังจากที่เรารู้คร่าวๆแล้วว่าการใช้ Matchers นั้นทำประมาณไหน ต่อไปเราจะมาลองดูตัวอย่างกัน โดยจะแบ่งตัว method ที่เป็น Matcher ออกมาเป็น 3 หมวดหมู่ Object, Core และ Logic มาลองดูกัน

หมวด Object

equalTo()
ทำงานเหมือน assertEqual() เลยครับ

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;

public class MyUnitTest {

	@Test
	public void testConcatenate() {
		MyUnit myUnit = new MyUnit();
		String result = myUnit.concatenate("one", "two");

		assertThat("onetwo", equalTo(result));
	}
}


instanceOf()

เป็น Matcher ไว้ Unit test ว่า object นั้นถูกสร้างมาจาก class ที่ระบุหรือเปล่า

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetTheObject() {
        MyUnit myUnit = new MyUnit();        
        assertThat(myUnit.getTheObject(),instanceOf(MyUnit.class));
    }
}

nullValue() + notNullValue()
ทำงานเหมือน assertNull() + assertNotNull() เลยครับ

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetTheObject() {
        MyUnit myUnit = new MyUnit();

        assertThat(myUnit.getTheObject(),nullValue());
        assertThat(myUnit.getTheObject(),notNullValue());
    }
}

sameInstance()
ทำงานเหมือน assertSame() เช่นกัน

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetTheObject() {
        MyUnit myUnit = new MyUnit();

        assertThat(myUnit.getTheObject(), sameInstance(myUnit.getTheObject()));

    }
}

WTF! ทำไม method ทำหน้าที่ซ้ำซ้อนกับ Junit เดิมอย่างงี้ฟ่ะ? ที่เป็นเช่นนั้นเพราะมันมาจาก framework คนละตัวทำให้มีมุมมองที่ต่างกัน ตัว JUnit คิดถึงการเทียบแบบตรงไปตรงมา ส่วน Hamcrest เป็นการคิดถึงการเอาผลลัพท์ไป match ด้วย Matcher ต่างๆ ซึ่งก็มีข้อดีแต่จะยังไงเรามาดูกันต่อ

หมวด Core

any()
คล้าย instanceOf() คือขอให้เป็น object ที่สร้างจาก class ที่ระบุโดยจะมีค่าเป็นไรก็ได้

ตัวอย่าง: ทำ Unit test ว่าค่าที่ส่งกลับมาเป็น String รวมถึง class แม่ก็คือ Object ด้วย

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetTheObject() {
        MyUnit myUnit = new MyUnit();

        assertThat(myUnit.getString(), any(String.class));
        assertThat(myUnit.getString(), any(Object.class));

    }
}


is()

ใช้ Unit test แทนได้ทั้ง equalTo() และ instanceOf()

เพื่ออะไร? ซ้ำกับ JUnit ยังพอเข้าใจ แต่ซ้ำซ้อนกับ method ของ framework ตัวเองมันยังไงๆอยู่?

จริงๆแล้ว Hamcrest มีความพยายามที่จะให้ code อ่านรู้เรื่องมากที่สุด(readable) เขาเรียกมันว่าน้ำตาล(Sugar) เรามาดูตัวอย่างเพื่อความเข้าใจมัน

ตัวอย่าง: ทำ Unit test ว่าค่าที่ส่งกลับมาเป็น String อะไรก็ได้

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetTheObject() {
        MyUnit myUnit = new MyUnit();
        String it = myUnit.getString();        

        // it is "Hello"        
        assertThat(it, is("Hello"));

        // it is String.class
        assertThat(it, is(String.class));
        
        // it is instance of String.class
        assertThat(it, is(instanceOf(String.class)));
                
        // it is any String.class
        assertThat(it, is(any(String.class)));        
    }
}

ตามโจทย์เราสามารถเขียนได้หลายวิธีตั้งแต่ใช้ instandOf(), any() แต่ is() จะมาช่วยสร้าง code เราให้เป็นประโยค(ภาษาอังกฤษ) เพิ่มขึ้น เช่นตัวอย่างนี้ ถ้าจะให้อ่านเป็นภาษาอังกฤษเพิ่มขึ้นเราก็จะใช้ assertThat(it, is(any(String.class))) = it is any String.class นั้นเอง

เนื่องจากมันเป็นน้ำตาลเท่านั้นโปรแกรมเมอร์บางท่านชอบกาแฟดำไม่ใส่น้ำตาลมากกว่าจะเลือกไม่ใส่ก็ได้ครับ 🙂

describedAs()
ใช้กรณีที่เราอยากใส่รายละเอียดของ Unit test มากเป็นพิเศษ
ตัวอย่าง: ทำ Unit test ว่าค่าที่ส่งกลับมาต้องไม่เท่ากับ null

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;

public class MyUnitTest {

    @Test
    public void testGetUnitName() {
        MyUnit myUnit = new MyUnit();
        assertThat(myUnit.getUnitName(), is(notNullValue()));
    }
    
    @Test
    public void testGetUnitNameByDesc() {
        MyUnit myUnit = new MyUnit();
        assertThat(myUnit.getUnitName(), describedAs("Unit name is not null", is(notNullValue())));
    }    
}

และพอ run ตัว Unit test (บทความนี้ผม run ด้วย maven test)โดยสมมุติว่าได้ค่า getUnitName() ได้ null ทั้งคู่ เรามาดู log กัน

หน้าตา log ตอน FAILURE:

testGetUnitName(MyUnitTest)  Time elapsed: 0.009 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: is not null
     got: null
...
testGetUnitNameByDesc(MyUnitTest)  Time elapsed: 0.001 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: Unit name is not null
     got: null
...

สังเกตุว่า testGetUnitName() บอกแค่ is not null แต่ testGetUnitNameBydesc() พ่นรายละเอียดที่เราต้องการออกมาทาง log ให้ด้วย ซึ่งเหมาะกับการ Unit test งานที่ซับซ้อนหรือที่เข้าใจยาก

หมวด Logical

allOf()
ใช้เมื่อเราต้องการ Unit test โดยเทียบกับ Matcher หลายๆตัว โดยเงื่อนไขที่จะผ่านนั้นคือต้อง match กับทุก Matcher จึงจะผ่าน มองเหมือนเอา Matcher มา AND กันนั้นเอง
ตัวอย่าง: ทำ Unit test ว่า getUnitName() ต้องห้ามเป็น null และมี email format ที่ถูกต้อง และเป็นไปตามกฎของการตั้ง user name
ปล.ตัวอย่างนี้สมมุติเราทำ validEmailFormat() กับ validUsernameFormat ขึ้นมาเอง (วิธี Custom Matcher ดูได้จากบทที่แล้ว)

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;

public class MyUnitTest {
    
    @Test
    public void testAllOf(){
    	
        MyUnit myUnit = new MyUnit();
        assertThat(myUnit.getUnitName(), allOf(is(String.class),is(CustomMatcher.validEmailFormat()),is(CustomMatcher.validUsernameFormat())));    	
    }
    
}

หน้าตา log ตอน FAILURE:

testAllOf(MyUnitTest)  Time elapsed: 0.014 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: (is an instance of java.lang.String and is invalid email format and is username format)
     got: "a#b.com"

anyOf()
คล้าย allOf() ต่างกันตรงที่แค่ match กับ Matcher ตัวใดตัวหนึ่งก็ Unit test ผ่านแล้ว มองเหมือนเอา Matcher มา OR กัน
ตัวอย่าง: ทำ Unit test ว่า getUnitStatus() ต้องเป็น approve หรือ reject อย่างใดอย่างหนึ่ง
ปล.ตัวอย่างนี้สมมุติเราทำ approveStatus() กับ rejectStatusขึ้นมาเอง

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;

public class MyUnitTest {

    @Test
    public void testAnyOf(){
    	
        MyUnit myUnit = new MyUnit();
        assertThat(myUnit.getUnitStatus(), anyOf(is(CustomMatcher.approveStatus()),is(CustomMatcher.rejectStatus())));    	
    }    
    
}

หน้าตา log ตอน FAILURE:

testAnyOf(MyUnitTest)  Time elapsed: 0.014 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: (is approve status or is reject status)
     got: "new"

not()
ให้ผลตรงข้ามกับ Matchar ที่ส่งเข้ามา
ตัวอย่าง: ทำ Unit test ว่า getUnitStatus() ต้องไม่ใช่ approve
ปล.ย้ำอีกทีว่า approveStatus() ไม่มีใน Hamcrest นะครับเป็น Matcher ที่ Custom เองเพื่อใช้ในตัวอย่าง

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;

public class MyUnitTest {

    @Test
    public void testAnyOf(){
    	
        MyUnit myUnit = new MyUnit();
        assertThat(myUnit.getUnitStatus(), is(not(CustomMatcher.approveStatus())));    	
    }    
    
}

หน้าตา log ตอน FAILURE:

testNot(MyUnitTest)  Time elapsed: 0.009 sec  <<< FAILURE!
java.lang.AssertionError: 
Expected: is not approve status
     got: "approve"

มาถึงตรงนี้เราคงพอเข้าใจคร่าวๆแล้วว่าเราจะเล่นกับตัว assertThat กับ Matcher ยังไง ต่อไปน่าจะแปล Testing for Exceptions By Jakob Jenkov ต่อนะครับ 🙂

ปล. ผู้เขียนอ่านไป แปลไป เขียนไป ผิดถูกยังไงแนะนำมาได้นะครับ

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s