Oop lab/unit testing slick2d
- หน้านี้เป็นส่วนหนึ่งของ oop lab.
เราต้องการจะเขียน unit test กับโค้ดใน Slick2D อย่างไรก็ตามโค้ดส่วนของ Entity ของเรานั้นมักมีส่วนการแสดงผลอยู่ด้วย และส่วนเหล่านี้โดยมากจะทำงานไม่ได้ใน unit test หรือไม่ก็ทำให้การเตรียมการต่าง ๆ มีปัญหา
เพื่อเพิ่มความเข้าใจ เราจะยกตัวอย่างจากโค้ด BulletGame แต่เราจะเพิ่มให้คลาส Bullet ใช้รูปลูกกระสุน แทนที่จะเป็นการวาดรูปธรรมดา
ในการทำ unit test นั้น เราจะทดลองเพิ่มเมท็อด distanceTo(float x, float y) ให้กับคลาส Bullet เป็นตัวอย่าง
เนื้อหา
Bullet ที่แสดงโดยการวาดรูป
เพื่อเป็นการทดลอง เราจะใช้รูป ship จาก ship เกมในการแสดงกระสุน เมื่อรันแล้วอาจจะมีหน้าตาแบบนี้
เราจะคัดลอกไฟล์ ship.png มาใส่ในไดเร็กทอรี res และเพิ่ม field image, แก้ constructor, และปรับเมท็อด render ในคลาส Bullet ดังนี้
public class Bullet implements Entity {
//...
protected Image image;
public Bullet(float x, float y) {
try {
image = new Image("res/ship.png"); // this line is problematic.
} catch (SlickException e) {
e.printStackTrace();
}
this.setXY(x,y);
}
public void render(Graphics g) {
image.draw(getX(), getY());
//g.fillOval(getX(), getY(), BULLET_SIZE, BULLET_SIZE); --- this is removed line
}
//...
}
หมายเหตุ: block try-catch นั้นอาจจะดูรกรุงรัง แต่จำเป็นเพราะว่าการเปิด image ใหม่นั้น อาจจะเกิด exception ขึ้นได้
ทดลอง unit test
ในโค้ด unit test ของเรา เราจะทดลองแค่สร้าง bullet มาก่อน
public class BulletTest {
@Test
public void testBulletCanBeCreated() {
Bullet b = new Bullet(10,20);
}
}
เมื่อเราเรียกให้ unit test ทำงาน เราจะพบ error ดังนี้
java.lang.RuntimeException: No OpenGL context found in the current thread. at org.lwjgl.opengl.GLContext.getCapabilities(GLContext.java:124) at org.lwjgl.opengl.GL11.glGetError(GL11.java:1289) at org.newdawn.slick.opengl.renderer.ImmediateModeOGLRenderer.glGetError(ImmediateModeOGLRenderer.java:384) at org.newdawn.slick.opengl.InternalTextureLoader.getTexture(InternalTextureLoader.java:249) at org.newdawn.slick.opengl.InternalTextureLoader.getTexture(InternalTextureLoader.java:187) ... at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
เพราะว่าจะเปิด Image ได้นั้น โปรแกรมของเราจะต้องมีการจัดการเปิดหน้าต่างและจัดการเกี่ยวกับ OpenGL ก่อน ซึ่งในส่วนนี้ เรามักจะไม่ดำเนินการใน Unit test
การเขียนโปรแกรมให้ test ได้ง่ายนั้น เป็นทักษะแบบหนึ่งที่มีประโยชน์มาก และโดยมาก ความพยายามที่จะทำให้โปรแกรม test ได้ง่าย จะทำให้เราได้รับโค้ดที่ดีมีคุณภาพด้วย
วิธีที่ 1: สร้าง Image ยามต้องการ
เพื่อให้โค้ด test ได้ โค้ดทุกส่วนที่เกี่ยวข้องกับการทำงานทั่วไป (ที่ไม่เกี่ยวกับการแสดงผล) จะต้องไม่มีการจัดการเกี่ยวกับการแสดงผลเลย และจะมีโค้ดเกี่ยวกับการแสดงผลเมื่อจำเป็น เช่นเมื่อมีการเรียก render แล้ว
เราจะแก้ constructor ของเราดังนี้
public Bullet(float x, float y) {
image = null;
this.setXY(x,y);
}
จากนั้นโค้ดที่เหลือทั้งหมดจะต้องไม่ยุ่งกับ image ยกเว้นโค้ดที่จำเป็นจะต้องเกี่ยวข้องกับเมท็อด render หรือถ้าจะมีการยุ่งเกี่ยว ต้องตรวจสอบก่อนว่า image ต้องไม่เป็น null ในส่วนนี้อาจจะทำได้ง่ายถ้าเป็นกรณีของคลาส Bullet แต่ถ้าเราต้องการให้ยานอวกาสหมุนหัวไปตามทิศทางด้วย (ในกรณีของ directional bullet) อาจจะมีความยุ่งเกิดขึ้นได้
เพื่อให้โปรแกรมเมื่อรันแสดงผลได้ เราจะแก้โค้ด render ให้สร้าง image ก่อนที่จะวาด
@Override
public void render(Graphics g) {
if (image == null) {
try {
image = new Image("res/ship.png");
} catch (SlickException e) {
e.printStackTrace();
}
}
image.draw(getX(), getY());
}
เมื่อเขียนส่วนนี้แล้ว โค้ดเราจะสามารถทำงานได้ และ unit test ก็สามารถรันได้แล้ว
เมื่อโค้ดรันได้ลักษณะนี้ เราก็สามารถเขียน test สำหรับทดสอบเมท็อด distanceTo ได้
วิธีที่ 2: Renderable
วิธีที่ 1 ก็เป็นทางเลือกที่ดี แต่อย่างไรก็ตาม โค้ดเกี่ยวกับการแสดงผลก็ยังอยู่ในโค้ดจัดการการทำงานอื่น ๆ และเมื่อโค้ดอยู่ร่วมกัน ในการทำงานหลาย ๆ อย่าง เราอาจจะอยาก "แอบ" ใช้งานข้อมูลจากส่วนแสดงผล ยกตัวอย่างเช่น ในกรณีของกระสุนที่หมุนได้ เราก็อาจจะอยากไปปรับทิศทางของรูป ในขณะที่กระสุนหมุน เป็นต้น
ดังนั้นเราจะแยกส่วน render ออกมาจากส่วน Entity ซึ่งการแก้ไขที่โครงสร้างระดับพื้นฐานนี้ จะทำให้เราต้องแก้โค้ดเป็นจำนวนมาก แต่อาจจะคุ้มค่า เพราะจะทำให้เราสามารถเทสโค้ดของเราได้ง่ายขึ้น
เราจะสร้าง interface Renderable ที่มีเมท็อด render
import org.newdawn.slick.Graphics;
public interface Renderable {
void render(Graphics g);
}
และแก้ interface Entity โดยตัดเมท็อด render ทิ้ง และเพิ่มเมท็อด getRenderable
public interface Entity {
Renderable getRenderable();
void update(int delta);
boolean isDeletable();
}
= แยก Bullet
BulletGame: update and render loop
เราจะเก็บวัตถุที่ใช้สำหรับการวาดรูปใน hash map renderables