Saladpuk.com
🏆 เนื้อหาหลัก
🏆 เนื้อหาหลัก
  • 💖สลัดผัก
  • 📰มีอะไรใหม่บ้าง
    • 2020
      • 2020-11
      • 2020-10
      • 2020-09
      • 2020-08
      • 2020-03
      • 2020-02
      • 2020-01
    • 2019
      • 2019-12
      • 2019-11
      • 2019-10
      • 2019-09
      • 2019-08
  • 🤔อ่านเรื่องไรดี ?
  • มือใหม่หัดเขียนโค้ด
    • 👶เขียนโค้ดด้วยภาษา C#
      • เกิดมาไม่เคยเขียนโค้ดมาก่อนเบย
      • 👶พื้นฐาน
        • 1.โปรแกรมที่ต้องลง
        • 2.โครงสร้างของโค้ด
        • 3.ชนิดของข้อมูล
        • 4.การสร้างตัวแปร
        • 5.คำสั่งพื้นฐาน
        • 6.การแปลงข้อมูล
        • 7.การเปรียบเทียบค่า
        • 8.การตัดสินใจด้วย IF statements
        • 9.การตัดสินใจด้วย Switch statements
        • 10.การทำงานซ้ำๆด้วย While
        • 11.การทำงานซ้ำๆด้วย Do While
        • 12.การทำงานซ้ำๆด้วย For
        • 13.การแก้โจทย์จากรูป
        • 14.มารู้จักกับ Array กัน
      • 🧑ระดับกลาง
        • 15.Value type vs Reference type
        • 16.ลดงานซ้ำๆด้วย Method
        • 17.มารู้จักกับ Class & Field กัน
        • 18.มารู้จักกับ Constructor กันบ้าง
        • 19.มาเขียน Method ใน Class กัน
        • 20.มารู้จักกับ Property กัน
        • 21.ลองใช้คลาสแบบจริงจังบ้าง
        • 22.การสืบทอด Inheritance
        • 23.Polymorphism
        • 24.Abstract Class
        • 25.Interface
        • 26.Namespace
        • 27.Enum
        • 28.Exception handler
        • 29.ลงลึกกับ string
        • 30.StringBuilder เพื่อนคู่ string
      • 👨⏳ระดับสูง
        • Generic
        • Delegates
        • Action & Func
        • Lambda expression
        • LINQ
        • พระคัมภีร์การใช้คำสั่ง LINQ
      • 💡Tips
        • 💡C# version 8.0
        • 💡Boxing & Unboxing
    • 👶Algorithm
      • 👾Algorithm Big-O
      • 👽Algorithm P & NP
    • 👦OOP
      • 💖Abstraction
      • 💖Encapsulation
      • 🏆Abstraction & Encapsulation
      • 💖Inheritance
      • 💖Polymorphism
      • 🏆Inheritance & Polymorphism
      • 📝ลองเขียน OOP ดูดิ๊
      • 👑OOP + Power of Design
      • 🥰เทคนิคในการออกแบบ
    • 👶บทสรุปฐานข้อมูล
      • เก็บรูปในฐานข้อมูล
      • Database indexing
      • การลบข้อมูล
    • 👦Communication Patterns
    • 👦Design Patterns
      • 🤰Creational Patterns
        • 🏭Factory Method
        • 🏭Abstract Factory
        • ☝️ Singleton Pattern
        • 🏗️ Builder Pattern
        • 🎎Prototype Pattern
      • 🧱Structural Patterns
        • 🔌Adapter Pattern
        • 📪Proxy Pattern
  • Puzzle
    • 🧠Challenges
      • 🐴Google ม้า 25 ตัว
      • 🌉Amazon เสา 2 ต้น
      • 🥇ทองเก๊
      • 💊ยาต้านโควิด
      • 🎩CP หมวก 5 ใบ
      • 🧓Einstein's Riddle 01
  • พื้นฐานที่ควรต้องรู้
    • 🐳Docker
      • 📦Docker Containers
      • 🃏Docker Exercise 01
      • 🛠️ Docker Tools
      • 🗃️ Docker Registry
      • 🖼️ Container Image
      • 📢Docker Push
      • 🔄WSL
    • 👶Clean Code
      • 🧓Uncle Bob - Clean Code
      • 🧓Uncle Bob - Comments
      • 🧓Uncle Bob - Naming
      • 🧓Uncle Bob - Mindset
      • 🧓Uncle Bob - TDD
    • 👶Code Smells
    • 👶สิ่งที่คนเขียนโค้ดมักเข้าใจผิด
    • 👶AI พื้นฐาน
    • 👶Git พื้นฐาน
      • Git branching strategy
    • 👶Cloud พื้นฐาน
    • 👶UML พื้นฐาน
      • Activity Diagram
      • Class Diagram
      • Sequence Diagram
      • Use case Diagram
      • บทสรุปการใช้ UML
    • 👶Data Scientist
      • การเลือก Algorithms ให้ AI (1/5)
      • การเตรียมข้อมูลให้ AI (2/5)
      • หลักการตั้งคำถามให้ AI (3/5)
      • แฉความลับของ AI Model (4/5)
      • หัดเขียน AI จาก AI ของคนอื่น (5/5)
    • 👶DevOps พื้นฐาน
    • 👶Docker ขั้นพื้นฐาน
      • Image and Container
      • แชร์ Docker Image ที่สร้างไว้
    • 👶Microservices พื้นฐาน
      • Microservices ที่ดีมีลักษณะยังไง
      • Microservices Tips
      • จาก Monolith สู่ Microservices
    • 👶ความรู้พื้นฐานในการทำเว็บ
    • 👦Bottlenecks of Software
      • หัวใจที่สำคัญที่สุดของฐานข้อมูล
    • 👦Agile Methodology
      • Agile in a Nutshell
      • Software Development Life Cycle
      • Code Review
    • 👦Security พื้นฐาน
      • การเก็บรหัสผ่านที่ถูกต้อง
      • Security in actions
        • Hash function
      • Security Principles
      • 😎The Matrix 1
      • 😎The Matrix 2
      • HTTPS in a nutshell
    • 👦SOLID Design Principles
      • มารู้จักกับ SOLID กันดีกว่า
      • Single-Responsibility Principle
      • Open/Closed Principle
      • Liskov Substitution Principle
      • Interface Segregation Principle
      • Dependency-Inversion Principle
  • Cloud Computing
    • 👶Microsoft Azure 101
      • สมัคร Microsoft Azure
      • รู้จักกับ Resource Groups
      • สร้างเว็บตัวแรกกัน
      • สร้าง Virtual Machine กัน
      • ประเภทของคลาว์เซอร์วิส
      • มาสร้าง Logic App กัน
      • มาสร้าง Function App กัน
      • คลาว์คิดเงินยังไง ?
      • Cloud Native
      • Guideline for Cloud scaling
      • Auto Scaling
    • 👶Azure App Services
    • 👶App Service Plan
    • 👶Azure Storage
      • Blob storage
        • ลองสร้างที่เก็บไฟล์กันเลย
        • เข้าใจ Blob storage ให้มากขึ้น
        • ลองเขียนโค้ดอัพโหลดไฟล์กันบ้าง
        • สร้างเว็บจากที่ฝากไฟล์บนคลาว์
    • 👶Azure Bot Service
      • Bot เข้าใจเราได้ยังไงกันนะ
    • 👶Azure Cognitive Services
      • การสร้าง Cognitive Services
      • การ Login ด้วยใบหน้า
      • อ่านลายมือจากรูปเป็นตัวอักษร (OCR)
      • เขียน AI แยกของต่างๆทำยังไง?
      • เขียนแอพ ทายอายุ บอกเพศ ง่ายจิ๊ดเดียว
      • เขียนแอพให้ AI อธิบายรูปเป็นภาษาคน
    • 👶Machine Learning Studio
      • มาสร้าง AI ของแท้ตัวแรกของเรากัน
      • สร้าง AI ตัดสินใจอนุมัติบัตรเครดิต 💳
      • ลองเรียกใช้ AI ของเรากัน
    • 👶Azure Service Fabric
      • สร้าง Service Fabric กัน
    • 👶Blockchain
      • Blockchain ทำงานยังไง ?
      • Consensus Algorithm คืออะไร ?
      • สร้าง Blockchain ใช้เองกัน !
      • หัดเขียน Smart Contract กัน
    • 👶Power BI
    • 👶Azure Web App
      • เซิฟเวอร์บนคลาว์ ราคา? ต่าง?
    • 👶Azure DevOps
      • เล่น Azure DevOps กัน
      • เล่นกับ Repository
      • ลองทำ Continuous Integration (CI)
      • ลองทำ Continuous Delivery (CD)
      • เล่น Kanban Board
    • 🤠Cloud Playground
      • การป้องกันความลับหลุดตอนที่ 1
      • การป้องกันความลับหลุดตอนที่ 2
      • การป้องกันความลับหลุดตอนที่ 3
      • การป้องกันความลับหลุดตอนจบ
  • Software Testing
    • 👦Test-First Design
    • 👦Test-Driven Development
      • 1.มารู้จักกับ TDD กันดีกว่า
      • 2.Test cases เขาเขียนกันยังไงนะ
      • 3.เครื่องมือในการทดสอบ
      • 4.การใช้ Theory และ InlineData
      • 5.โค้ดที่ทดสอบได้
      • 6.Mantra of TDD
      • 7.Functional & None-Functional testing
      • 8.Manual vs Automation testing
      • 9.Automation Frameworks in .NET
      • 10.Mock Framework
      • 11.มาเรียนการใช้ Moq กันเถอะ
      • 12.สรุป
  • Web
    • 👦Web API
      • 1.Web API คืออะไร
      • 2.ติดตั้ง .NET Core SDK
      • 3.สร้าง Web API ตัวแรกกัน
      • 4.Verbs
      • 5.Swagger เพื่อคู่ API
      • 6.การใช้ Model
      • 7.เรียก Web API ผ่าน Postman
      • 8.มาจัดกลุ่ม API กัน (1/2)
      • 9.มาจัดกลุ่ม API กัน (2/2)
  • Software Design
    • 🤴Design Patterns
      • 🦈Creational patterns
        • Abstract Factory
        • Builder
        • Factory Method
        • Prototype
        • Singleton
      • 🦈Structural patterns
        • Adapter
        • Bridge
        • Decorator
        • Facade
        • Proxy
      • 🦈Behavioral patterns
        • Chain of Responsibility
        • Command
        • Iterator
        • Mediator
        • Memento
        • Observer
        • State
        • Strategy
        • Template Method
        • Visitor
Powered by GitBook
On this page
  • 🎮 Game Start !
  • 1.Design scenarios
  • 2.Testable code
  • 🎯 บทสรุป
  • 👨‍🚀 หัวใจหลักในการทำ TDD
  • 👨‍🚀 ดาวโหลดตัวอย่างทั้งหมด
  • 👨‍🚀 คอร์สเรื่องการทำ TDD แบบเต็มสูบ

Was this helpful?

Export as PDF
  1. Software Testing

Test-First Design

โชว์พลังที่แท้จริงของ Test-Driven Development (TDD) ด้วยเกม OX

Previousการป้องกันความลับหลุดตอนจบNextTest-Driven Development

Last updated 5 years ago

Was this helpful?

รอบนี้เราจะมาดูพลังที่แท้จริงในการใช้ Test-Driven Development (TDD) ว่ามันจะมาช่วยเรื่อง Code Design ได้ยังไง!! โดยใช้เกม OX เป็นโจทย์ในการเขียนโปรแกรม เพื่อให้เพื่อนๆได้ลองเอาไปศึกษาปรับใช้ดู แล้วจะรู้ว่าการเขียนเทสก่อนมันทรงพลังขนาดไหน

ความเข้าใจผิดกับการเขียนเทส Developer หลายๆคนจะบ่นว่า "การเขียนเทสทำให้งานช้า" เลยหลีกเลี่ยงไม่ยอมเขียนเทสก่อนเขียนโค้ด แต่ในขณะที่ครูระดับระดับตำนานและนักเขียนโค้ดระดับเทพทุกคนเขาเขียนเทสกันก่อนเขียนโค้ดทุกคน และงานส่วนใหญ่ก็ชี้ออกมาแล้วว่า "การเขียนเทสก่อนสุดท้ายมันเร็วกว่าการไม่เขียนเทสเยอะมาก"

🎮 Game Start !

1.Design scenarios

🧔 หลายๆคนพอรู้ว่าจะเขียนเกม OX ก็จะนึกภาพ class diagram หรือโค้ดต่างๆไว้ในหัวกันแล้วใช่ไหมล่ะ ?

🧔 แต่ปรกติการทำ Test First ขั้นตอนแรกเราจะไม่เขียนโค้ด และไม่เขียน Diagrams กันนะ!! แต่สิ่งที่เราจะเขียนเป็นตัวแรกคือสิ่งที่เขาเรียกกันว่า เทสเคส หรือ Scenarios นั่นเอง ซึ่งเทสเคสมันแบ่งออกเป็น 3 กลุ่มง่ายตามนี้

  1. กลุ่มปรกติที่เห็นได้บ่อยๆ (Normal cases)

  2. กลุ่มที่นานๆจะเจอที (Alternative cases)

  3. กลุ่มที่เกิดสถานะการณ์แปลกๆในในโปรแกรม (Exception cases)

🧔 ถัดมาเราจะต้องคิดเหตุการณ์ทั้งหมดที่จะเกิดขึ้นกับเกมของเราแล้วเอาไปใส่ในแต่ละกลุ่ม ซึ่งเหตุการณ์ที่จะใส่จะต้องบอกรายละเอียดตั้งแต่ต้นจนจบว่าจะทำอะไรและเกิดผลลัพท์อะไรด้วย มาเดี๋ยวจะลองใส่ตัวอย่างกลุ่มแรกให้

กลุ่มปรกติที่เห็นได้บ่อยๆ (Normal cases)
------
1.ลง X ลงไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
2.ลง O ลงไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X

🧔 ข้อดีในการทำแบบนี้คือเราสามารถเห็นข้อผิดพลาดได้เลยโดยที่ยังไม่ต้องเขียนโค้ดด้วยซ้ำ เช่น กฎิกาของเกม OX จริงๆจะต้องให้ X ลงก่อน ดังนั้นกรณีที่ 2 จะต้องไม่มี เพราะ O ลงก่อนไม่ได้! ดังนั้นก็ลบข้อ 2 ออกซะ

กลุ่มปรกติที่เห็นได้บ่อยๆ (Normal cases)
------
1.ลง X ลงไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O

ป้องกันการเข้าใจผิดก่อนที่จะเขียนโค้ด คุณคิดว่าไปไล่อ่านโค้ดที่เขาเขียนเพื่อตรวจว่าคนเขียนเข้าใจงานถูกหรือเปล่าได้ง่ายไหม? เมื่อเทียบกับไปไล่ตรวจจากเทสเคสก่อน อันไหนง่ายกว่ากัน ?

🧔 ตรงนี้เราจะคิดเหตุการณ์ของ กลุ่มปรกติที่เห็นได้บ่อยๆ กันก่อน เพราะคนใช้จะต้องเจอเคสนี้บ่อยสุดราวๆ 80% ดังนั้นถ้าเราไล่เก็บเคสนี้ก่อน มันหมายความว่าน่าจะไม่มี bug กับงานพื้นฐานของแอพเราแล้วนั่นเอง ปะลองไล่ดูว่ามีไรบ้าง

กลุ่มปรกติที่เห็นได้บ่อยๆ (Normal cases)
------
1.ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
2.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
3.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
4.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
5.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
6.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว แต่ O ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
7.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว และ X ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า X ชนะพร้อมกับเพิ่มแต้มให้ X 1 คะแนน
8.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว และ O ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า O ชนะ พร้อมกับเพิ่มแต้มให้ O 1 คะแนน
9.ลง X ไปในช่องว่างซึ่งบนกระดานมี X 4 ตัวและ O 4 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ และแจ้งว่าเกมเสมอ

ไม่ต้องนั่งคิดให้มันครบทุกเคสก็ได้นะ เอาแค่ที่นึกออกก็พอ คิดออกเมื่อไหร่ค่อยมาเพิ่มอีกหลังก็ได้

🧔 เราก็จะได้เทสเคสของกรณีที่เกิดขึ้นบ่อยๆออกมาราวๆด้านบนนี้ ถัดไปเราก็จะลองมาคิด กลุ่มนานๆจะเจอที กันบ้าง เพราะมันจะเป็นการเพิ่มความมั่นใจว่าถ้าเกิดเคสที่นานๆจะเจอที อย่างน้อยมันก็จะทำงานได้นั่นเอง ซึ่งพอไปคิดคร่าวๆมาก็น่าจะได้ราวๆนี้ (ไม่ต้องรีดสมองคิดให้คลุมทั้งหมดก็ได้ มาเติมเอาทีหลังเช่นเคยก็โอเคนะ)

กลุ่มที่นานๆจะเจอที (Alternative cases)
-----
1.ลง X ไปในช่องที่ไม่ว่าง ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ X อยู่
2.ลง O ไปในช่องที่ไม่ว่าง ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ O อยู่

🧔 ถัดไปก็ กลุ่มที่เกิดสถานะการณ์แปลกๆในในโปรแกรม ซึ่งจะเพิ่มความมั่นใจว่าถ้าเจออะไรแปลกๆ อย่างน้อยเราแอพเราก็น่าจะรับมือได้ในระดับนึงนั่นเอง ซึ่งคิดคร่าวๆก็จะได้ราวๆนี้

กลุ่มที่เกิดสถานะการณ์แปลกๆในในโปรแกรม
-----
1.ลง X ไปในช่องว่างซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบไม่ให้ลงพร้อมแจ้งเตือน และสลับเป็นตาของ O
2.ลง X ไปในช่องที่ไม่มีอยู่ในกระดาน ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ X อยู่

🧔 จากที่ทำมาทั้งหมดเราก็จะได้ เทสเคส ของตัวโปรแกรมอย่างง่ายๆออกมาแล้ว ซึ่งหน้าตาก็ประมาณนี้

กลุ่มปรกติที่เห็นได้บ่อยๆ (Normal cases)
-----
1.ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
2.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
3.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
4.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
5.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O
6.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว แต่ O ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X
7.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว และ X ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า X ชนะพร้อมกับเพิ่มแต้มให้ X 1 คะแนน
8.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว และ O ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า O ชนะ พร้อมกับเพิ่มแต้มให้ O 1 คะแนน
9.ลง X ไปในช่องว่างซึ่งบนกระดานมี X 4 ตัวและ O 4 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ และแจ้งว่าเกมเสมอ

กลุ่มที่นานๆจะเจอที (Alternative cases)
-----
10.ลง X ไปในช่องที่ไม่ว่าง ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ X อยู่
11.ลง O ไปในช่องที่ไม่ว่าง ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ O อยู่

กลุ่มที่เกิดสถานะการณ์แปลกๆในในโปรแกรม
-----
12.ลง X ไปในช่องว่างซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบไม่ให้ลงพร้อมแจ้งเตือน และสลับเป็นตาของ O
13.ลง X ไปในช่องที่ไม่มีอยู่ในกระดาน ระบบไม่ให้ลงพร้อมแจ้งเตือน และยังคงเป็นตาของ X อยู่

2.Testable code

🧔 ถัดมาเราก็จะเอา เทสเคส จากขั้นตอนที่ 1 มาแปลงเป็นโค้ดที่เอาไว้ทดสอบโปรแกรมของเรา โดยเราจะเอามาทีละข้อ ไล่จากบนลงล่างเลย ดังนั้นข้อที่ 1 ของเราคือ

1.ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O

เราก็จะเขียนเทสให้กับข้อนี้ก่อน ได้ตามนี้ (ในตัวอย่างผมใช้ภาษา C# กับ xUnit นะครับ)

[Fact(DisplayName = "ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXWhenBoardIsEmpty()
{
    var boardGame = new BoardGame();
    var canPlace = boardGame.Place("X", 0, 0);
    Assert.True(canPlace);
    Assert.Equal("O", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}
public string CurrentTurn { get; set; }

public bool Place(string symbol, int row, int column)
{
    throw new NotImplementedException();
}

public string GetWinner()
{
    throw new NotImplementedException();
}

ตัวอย่างโค้ดมันจะมีชื่อไฟล์อยู่นะ ให้กดชื่อไฟล์เพื่อนดูโค้ดในไฟล์นั้น + ตัวอย่างโค้ดผมจะเอาเฉพาะที่สำคัญมาให้ดูนะ จะได้ focus ได้ถูกจุดไม่งั้น งง ตายเลย

🧔 ซึ่งพอ run test มันก็จะ Fail ครับ เพราะโค้ดใน BoardGame.cs ยังไม่ได้เขียนอะไรเลย ดังนั้นเราก็จะเขียนแบบง่ายที่สุดเพื่อให้มันผ่านเทส เราก็จะได้โค้ดประมาณนี้

BoardGame.cs
public string CurrentTurn { get; set; }

public bool Place(string symbol, int row, int column)
{
    CurrentTurn = "O";
    return true;
}

public string GetWinner()
{
    return null;
}

เขียนโค้ดให้น้อยที่สุดเท่าที่จะทำได้ เพื่อให้เทสผ่าน เพราะเราจะได้โค้ดที่ไม่ซับซ้อนอะไรเลยออกมาก่อน

🧔 เย่เทสผ่านละ!! ต่อมาเราก็จะเอาเคสตัวถัดไปเข้ามาเพิ่ม ซึ่งตัวถัดไปก็คือ

2.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X

เราก็จะเขียนเทสเคสให้ตัวนี้ต่อ ได้ตามนี้

[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X")]
public void PlaceOInEmptySlotWhenBoardHave_1X_0O()
{
    var boardGame = new BoardGame
    {
        Slots = new string[,]
        {
            { "X", null, null },
            { null, null, null },
            { null, null, null },
        }
    };
    var canPlace = boardGame.Place("O", 0, 1);
    Assert.True(canPlace);
    Assert.Equal("X", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}
public string[,] Slots { get; set; }
public string CurrentTurn { get; set; }

public bool Place(string symbol, int row, int column)
{
    CurrentTurn = "O";
    return true;
}

public string GetWinner()
{
    return null;
}

🧔 ซึ่งพอเอาไป Run มันก็จะไม่ผ่าน เพราะคลาส GameBoard ยังเขียนแบบกากๆ ดังนั้นเราก็ต้องไปแก้ให้มันผ่านเคสนี้และเคสก่อนหน้าด้วยโดยเขียนให้ง่ายที่สุด

ในรอบนี้ผมจะคำนวณว่าเป็นตาของใครจาก Slots โดยมันจะนับว่า Array ที่ไม่เป็น null เป็นเลขคู่หรือเลขคี่ เพราะถ้าเป็นเลขคู่แสดงว่าเป็นตาของ X แต่ถ้าเป็นเลขคี่แสดงว่าเป็นตาของ O นั่นเอง

GameBoard.cs
public string[,] Slots { get; set; }
public string CurrentTurn { get; set; }

public BoardGame()
{
    Slots = new string[3, 3];
}

public bool Place(string symbol, int row, int column)
{
    Slots[row, column] = symbol;

    var counter = 0;
    foreach (var item in Slots)
    {
        if (item != null)
        {
            counter++;
        }
    }

    if (counter % 2 == 0)
    {
        CurrentTurn = "X";
    }
    else
    {
        CurrentTurn = "O";
    }

    return true;
}

🧔 ในตอนนี้ทั้งสองเคสก็จะทำงานผ่านหมดละ แต่โค้ดน่าเกลียดมาก ดังนั้นผมจะทำการปรับโค้ดให้อ่านง่ายขึ้นกว่าเดิมหน่อย ซึ่งเราเรียกขั้นตอนนี้ว่า Refactor

Refactor คือการปรับโค้ดให้มันดียิ่งขึ้นกว่าเดิม ซึ่งการจะทำ Refactor ได้โค้ดจะต้องถูกเขียนเทสเคสคลุมไว้แล้ว เพราะ ถ้าเราปรับโค้ดไปแล้ว เราจะรู้ได้ไงว่าไอ้ที่ปรับไปมันยังทำงานถูกอยู่? ดังนั้นมันเลยต้องมีเทสเคสเป็นตัวช่วยเช็คว่ายังทำงานถูกเหมือนเดิมหรือเปล่านั้นเอง

🧔 ขั้นตอนแรกผมก็ Refactor ตัว loop ว่าเป็นเลขคู่เลขคี่หรือเปล่าในไฟล์ GameBoard.cs บรรทัดที่ 13~20 ซึ่งก็จะได้โค้ดใหม่ออกมาเป็นแบบนี้

GameBoard.cs
public bool Place(string symbol, int row, int column)
{
    Slots[row, column] = symbol;

    var isEvenNumber = Slots.Cast<string>().Count(it => it != null) % 2 == 0;

    if (isEvenNumber)
    {
        CurrentTurn = "X";
    }
    else
    {
        CurrentTurn = "O";
    }

    return true;
}

🧔 ลอง Run test ละก็ผ่าน ดังนั้นผมก็จะ Refactor ต่อกับไฟล์เดิมนี่แหละ เพราะผมคิดว่า การตรวจว่าเป็นตาของ X หรือ O ในบรรทัดที่ 7~14 ยังเยิ่นเย้ออยู่ ซึ่งก็จะ Refactor ใหม่ออกมาได้เป็นแบบนี้

GameBoard.cs
public bool Place(string symbol, int row, int column)
{
    Slots[row, column] = symbol;

    var isEvenNumber = Slots.Cast<string>().Count(it => it != null) % 2 == 0;
    CurrentTurn = isEvenNumber ? "X" : "O";

    return true;
}

🧔 อะเช Run test แล้วก็ผ่านอยู่ งั้นตอนนี้ไปเอาเทสเคสที่ 3 มาทำต่อบ้างดีกว่า ซึ่งมันเขียนไว้ว่า

3.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O

ดังนั้นผมก็จะเขียนเทสให้ตัวนี้ออกมาเป็นตามนี้

BoardGameTest.cs
[Fact(DisplayName = "ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXInEmptySlotWhenBoardHave_1X_1O()
{
    var boardGame = new BoardGame
    {
        Slots = new string[,]
        {
            { "X", "O", null },
            { null, null, null },
            { null, null, null },
        }
    };
    var canPlace = boardGame.Place("X", 1, 0);
    Assert.True(canPlace);
    Assert.Equal("O", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}

🧔 แล้วก็ลอง Run test ก็จะพบว่ามันผ่านเหมือนกัน แต่สิ่งที่ผมเห็นแล้วน่ารำคาญคือเจ้าไฟล์ BoardGameTest.cs เพราะทุกครั้งที่ผมเขียนเทส มันจะดูเหมือนมันเขียนของเดิมซ้ำๆ ไม่เชื่อลองดูไฟล์เต็มๆมันดูนะ

BoardGameTest.cs
[Fact(DisplayName = "ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXWhenBoardIsEmpty()
{
    var boardGame = new BoardGame();
    var canPlace = boardGame.Place("X", 0, 0);
    Assert.True(canPlace);
    Assert.Equal("O", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}

[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X")]
public void PlaceOInEmptySlotWhenBoardHave_1X_0O()
{
    var boardGame = new BoardGame
    {
        Slots = new string[,]
        {
            { "X", null, null },
            { null, null, null },
            { null, null, null },
        }
    };
    var canPlace = boardGame.Place("O", 0, 1);
    Assert.True(canPlace);
    Assert.Equal("X", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}

[Fact(DisplayName = "ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXInEmptySlotWhenBoardHave_1X_1O()
{
    var boardGame = new BoardGame
    {
        Slots = new string[,]
        {
            { "X", "O", null },
            { null, null, null },
            { null, null, null },
        }
    };
    var canPlace = boardGame.Place("X", 1, 0);
    Assert.True(canPlace);
    Assert.Equal("O", boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}

🧔 สังเกตุดีๆจะเห็นว่าข้างใน method ทั้ง 3 ตัวมันเขียนเกือบจะเหมือนกันเลย นั่นแสดงว่าทุกๆครั้งที่ผมจะเอาเทสเคสมาเพิ่ม ผมก็จะเขียนของที่คล้ายๆเดิมไปลงเรื่อยๆ ทำให้โค้ดมันรก ดังนั้นในรอบนี้ผมก็จะทำการ Refactor ในฝั่งของตัว Test บ้างละ

สิ่งที่ผมจะทำคือรวมทั้ง 3 method ให้กลายเป็น method เดียว แล้วส่ง parameter แบบต่างๆเข้าไปแทน ก็จะได้โค้ดออกมาเป็นแบบนี้

BoardGameTest.cs
[Fact(DisplayName = "ลง X ไปตอนที่กระดานว่าง ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXWhenBoardIsEmpty()
{
    var slots = new string[3, 3];
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "X", 0, 0, "O");
}

[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวเท่านั้น ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X")]
public void PlaceOInEmptySlotWhenBoardHave_1X_0O()
{
    var slots = new string[,]
    {
        { "X", null, null },
        { null, null, null },
        { null, null, null },
    };
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "O", 0, 1, "X");
}

[Fact(DisplayName = "ลง X ไปในช่องว่าซึ่งบนกระดานมี X 1 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXInEmptySlotWhenBoardHave_1X_1O()
{
    var slots = new string[,]
    {
        { "X", "O", null },
        { null, null, null },
        { null, null, null },
    };
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "X", 1, 0, "O");
}

private void verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(string[,] slots, string symbol, int row, int column, string expectedCurrentTurn)
{
    var boardGame = new BoardGame { Slots = slots };
    var canPlace = boardGame.Place(symbol, row, column);
    Assert.True(canPlace);
    Assert.Equal(expectedCurrentTurn, boardGame.CurrentTurn);
    Assert.Null(boardGame.GetWinner());
}

Refactor เวลาที่ทำ Refactor สามารถทำได้ทั้ง 2 ฝั่งทั้ง โค้ดที่ถูกเทส และ โค้ดที่เอาไว้เทส แต่เวลาทำ Refactor แต่ละรอบ ต้องทำ Refactor ทีละฝั่งเท่านั้น ห้ามทำพร้อมกัน ไม่งั้นเราจะไม่รู้ว่ามันพังเพราะอะไรกันแน่

🧔 แน่นอนถ้าผมเปลี่ยนเป็นแบบนี้ก็ต้องลอง Run test ให้มันผ่านด้วยเช่นกัน ซึ่งก็ผ่านตามที่คาดไว้ ดังนั้นผมก็จะเริ่มเอาเทสเคสที่ 4~6 ลงมาใส่ต่อเลย (เพราะผมรู้ว่ามันก็ผ่านเหมือนกัน)

4.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X

5.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O

6.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว แต่ O ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X

ซึ่งก็จะได้โค้ดออกมาตามนี้

BoardGameTest.cs
[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 1 ตัว ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X")]
public void PlaceOInEmptySlotWhenBoardHave_2X_1O()
{
    var slots = new string[,]
    {
        { "X", "O", null },
        { "X", null, null },
        { null, null, null },
    };
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "O", 1, 1, "X");
}

[Fact(DisplayName = "ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ O")]
public void PlaceXInEmptySlotWhenBoardHave_2X_2O_ButNotConnectedTogather()
{
    var slots = new string[,]
    {
        { "X", "O", null },
        { "X", "O", null },
        { null, null, null },
    };
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "O", 0, 2, "O");
}

[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว แต่ O ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ แต่ยังไม่มีผู้ชนะ และสลับเป็นตาของ X")]
public void PlaceOInEmptySlotWhenBoardHave_3X_2O_ButNotConnectedTogather()
{
    var slots = new string[,]
    {
        { "X", "O", null },
        { "X", "O", null },
        { null, "X", null },
    };
    verifyPlaceASymbolToEmptySpaceThenSystemMustAcceptTheRequest(slots, "O", 2, 0, "X");
}

🧔 จากนั้นเราก็จะเริ่มเอาเทสเคสที่ 7 มาทำต่อ ซึ่งมันเป็นเทสเคสแรกที่มีคนชนะ โดยมันเขียนไว้ว่า

7.ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว และ X ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า X ชนะพร้อมกับเพิ่มแต้มให้ X 1 คะแนน

ดังนั้นผมก็จะเอาไปสร้างเทสเคสออกมาเป็นแบบนี้

[Fact(DisplayName = "ลง X ไปในช่องว่าซึ่งบนกระดานมี X 2 ตัวและ O 2 ตัว และ X ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า X ชนะพร้อมกับเพิ่มแต้มให้ X 1 คะแนน")]
public void PlaceXInEmptySlotWhenBoardHave_2X_2O_WithConnectedTogather()
{
    var slots = new string[,]
    {
        { "X", "O", null },
        { "X", "O", null },
        { null, null, null },
    };
    var boardGame = new BoardGame { Slots = slots };
    var canPlace = boardGame.Place("X", 2, 0);
    Assert.True(canPlace);
    Assert.Equal("X", boardGame.GetWinner());
    Assert.Equal(0, boardGame.OScore);
    Assert.Equal(1, boardGame.XScore);
}
public int OScore { get; set; }
public int XScore { get; set; }
public string[,] Slots { get; set; }
public string CurrentTurn { get; set; }

public BoardGame()
{
    Slots = new string[3, 3];
}

public bool Place(string symbol, int row, int column)
{
    Slots[row, column] = symbol;

    var isEvenNumber = Slots.Cast<string>().Count(it => it != null) % 2 == 0;
    CurrentTurn = isEvenNumber ? "X" : "O";

    return true;
}

public string GetWinner()
{
    return null;
}

🧔 ซึ่งพอเอาไป Run test มันก็จะ Fail เพราะ BoardGame.cs ยังไม่ถูกเขียนการคำนวณว่าใครชนะ และ ยังไม่มีการจัดการเรื่องคะแนน ดังนั้นผมเลยต้องเขียนแบบง่ายที่สุดให้มันผ่าน ซึ่งก็จะออกมาเป็นแบบนี้

BoardGame.cs
public string GetWinner()
{
    var firstRow = Slots[0, 0] + Slots[0, 1] + Slots[0, 2];
    var secondRow = Slots[1, 0] + Slots[1, 1] + Slots[1, 2];
    var thirdRow = Slots[2, 0] + Slots[2, 1] + Slots[2, 2];
    var firstColumn = Slots[0, 0] + Slots[1, 0] + Slots[2, 0];
    var secondColumn = Slots[0, 1] + Slots[1, 1] + Slots[2, 1];
    var thirdColumn = Slots[0, 2] + Slots[1, 2] + Slots[2, 2];
    var crossTop = Slots[0, 0] + Slots[1, 1] + Slots[2, 2];
    var crossBottom = Slots[2, 0] + Slots[1, 1] + Slots[0, 2];

    var allPossibilities = new string[]
    {
        firstRow, secondRow, thirdRow,
        firstColumn, secondColumn, thirdColumn,
        crossTop, crossBottom
    };

    foreach (var item in allPossibilities)
    {
        if (item.Length < 3)
        {
            continue;
        }

        if (item[0] == item[1] && item[0] == item[2])
        {
            var winnerSymbol = item[0].ToString();
            if (winnerSymbol == "X")
            {
                XScore++;
            }
            return winnerSymbol;
        }
    }

    return null;
}

ไม่ต้องสนใจว่าโค้ดจะน่าเกลียดขนาดไหนขอแค่มันทำงานได้ก็ OK แล้ว แต่ถ้าเขียนแบบลัดได้ก็เขียนไปเลย ที่ผมเขียนแบบกากๆให้ดูเพราะอยากให้ดูการ Refactor

🧔 จากที่เขียนมามันก็ OK นะเพราะมัน Run test ผ่าน แต่โค้ดแบบว่าฝุดๆอ่ะ อ่านก็ยาก ถ้ามันผิดมานี่ผมคงขี้เกียจไปแก้มันแน่ ดังนั้นขอ Refactor มันหน่อยละกัน ซึ่งสิ่งที่ผมจะทำก็คือทำให้โค้ด บรรทัดที่ 3~17 อ่านแล้วเป็นภาษามนุษย์ขึ้นมาหน่อย ซึ่งก็จะได้ออกมาเป็นแบบนี้

BoardGame.cs
public string GetWinner()
{
    var allPossibilities = getRowPossibilities()
        .Union(getColumnPossibilities())
        .Union(getCrossLinePossibilities());

    foreach (var item in allPossibilities)
    {
        if (item.Length < 3)
        {
            continue;
        }

        if (item[0] == item[1] && item[0] == item[2])
        {
            var winnerSymbol = item[0].ToString();
            if (winnerSymbol == "X")
            {
                XScore++;
            }
            return winnerSymbol;
        }
    }

    return null;
}

private IEnumerable<string> getRowPossibilities()
{
    return new string[]
    {
        $"{Slots[0,0]}{Slots[0,1]}{Slots[0,2]}",
        $"{Slots[1,0]}{Slots[1,1]}{Slots[1,2]}",
        $"{Slots[2,0]}{Slots[2,1]}{Slots[2,2]}",
    };
}

private IEnumerable<string> getColumnPossibilities()
{
    return new string[]
    {
        $"{Slots[0,0]}{Slots[1,0]}{Slots[2,0]}",
        $"{Slots[0,1]}{Slots[1,1]}{Slots[2,1]}",
        $"{Slots[0,2]}{Slots[1,2]}{Slots[2,2]}",
    };
}

private IEnumerable<string> getCrossLinePossibilities()
{
    return new string[]
    {
        $"{Slots[0,0]}{Slots[1,1]}{Slots[2,2]}",
        $"{Slots[2,0]}{Slots[1,1]}{Slots[0,2]}",
    };
}

🧔 อ่าเทสผ่านละ จะเห็นว่าผม Refactor การสร้าง possibilities ของเกม ให้เป็นภาษามนุษย์แล้ว คือบรรทัดที่ 3 อันเดียวจบ ส่วน method ที่โดนสร้างเพิ่มขึ้นมาปล่อยมันไปเลยเพราะหน้าที่มันสมบูรณ์ในตัวแล้ว (ผมไม่ได้จะมาสอนเขียน LinQ นะดังนั้นขอปล่อยไว้แบบนี้แหละ)

🧔 ถัดมาผมก็จะ Refactor บรรทัดที่ 7~25 เพื่อให้อ่านแล้วเข้าใจได้ง่ายขึ้น ว่ากำลังหาผู้ชนะ ซึ่งก็จะออกมาเป็นแบบโค้ดด้านล่างนี้

BoardGame.cs
public string GetWinner()
{
    var allPossibilities = getRowPossibilities()
        .Union(getColumnPossibilities())
        .Union(getCrossLinePossibilities());

    const int MinimumRequiredCharacters = 3;
    var winnerSpot = allPossibilities
        .Where(it => it.Length == MinimumRequiredCharacters)
        .FirstOrDefault(it => it.All(c => c == it.First()));

    var winnerSymbol = winnerSpot?.First().ToString();

    const string XSymbol = "X";
    if (winnerSymbol == XSymbol)
    {
        XScore++;
    }

    return winnerSymbol;
}

🧔 เทสผ่านเช่นเคย ดังนั้นผมก็จะลองเอาเทสเคส 8 มาลงต่อเลยละกัน

8.ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว และ O ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า O ชนะ พร้อมกับเพิ่มแต้มให้ O 1 คะแนน

BoardGameTest.cs
[Fact(DisplayName = "ลง O ไปในช่องว่าซึ่งบนกระดานมี X 3 ตัวและ O 2 ตัว และ O ทั้งหมดเรียงกัน ระบบให้ลงได้และประกาศว่า O ชนะ พร้อมกับเพิ่มแต้มให้ O 1 คะแนน")]
public void PlaceOInEmptySlotWhenBoardHave_3X_2O_WithConnectedTogather()
{
    var slots = new string[,]
    {
        { "X", "O", "X" },
        { "X", "O", null },
        { null, null, null },
    };
    var boardGame = new BoardGame { Slots = slots };
    var canPlace = boardGame.Place("O", 2, 1);
    Assert.True(canPlace);
    Assert.Equal("O", boardGame.GetWinner());
    Assert.Equal(1, boardGame.OScore);
    Assert.Equal(0, boardGame.XScore);
}

🧔 แต่มันก็จะยังไม่ผ่าน เพราะผมยังไม่ได้เขียน เพิ่มคะแนนให้กับ O เลย ดังนั้นก็ไปทำให้มันผ่านซะ

BoardGame.cs
public string GetWinner()
{
    var allPossibilities = getRowPossibilities()
        .Union(getColumnPossibilities())
        .Union(getCrossLinePossibilities());

    const int MinimumRequiredCharacters = 3;
    var winnerSpot = allPossibilities
        .Where(it => it.Length == MinimumRequiredCharacters)
        .FirstOrDefault(it => it.All(c => c == it.First()));

    var winnerSymbol = winnerSpot?.First().ToString();

    const string XSymbol = "X";
    const string OSymbol = "O";
    if (winnerSymbol == XSymbol)
    {
        XScore++;
    }
    else if(winnerSymbol == OSymbol)
    {
        OScore++;
    }

    return winnerSymbol;
}

🧔 ผ่านเรียบร้อยแล้วนะ ตอนนี้กลุ่มปรกติก็เหลือแค่เคส 9 อันสุดท้ายละ

9.ลง X ไปในช่องว่างซึ่งบนกระดานมี X 4 ตัวและ O 4 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ และแจ้งว่าเกมเสมอ

[Fact(DisplayName = "ลง X ไปในช่องว่างซึ่งบนกระดานมี X 4 ตัวและ O 4 ตัว แต่ X ทั้งหมดไม่ได้เรียงกัน ระบบให้ลงได้ และแจ้งว่าเกมเสมอ")]
public void PlaceXInEmptySlotWhenBoardHave_4X_4O_WithConnectedTogather()
{
    var slots = new string[,]
    {
        { "X", "O", "X" },
        { "X", "O", "O" },
        { "O", "X", null },
    };
    var boardGame = new BoardGame { Slots = slots };
    var canPlace = boardGame.Place("X", 2, 2);
    Assert.True(canPlace);
    Assert.Null(boardGame.GetWinner());
    Assert.Equal(0, boardGame.OScore);
    Assert.Equal(0, boardGame.XScore);
    Assert.True(boardGame.IsDraw);
}
public bool IsDraw { get; set; } // มีแค่อันนี้พี่เพิ่มเข้ามา

🧔 แล้วก็ Fail ตามที่คาด ดังนั้นก็ไปทำให้ผ่านครับ

BoardGame.cs
public string GetWinner()
{
    // ...
    else if (winnerSymbol == OSymbol)
    {
        OScore++;
    }
    else
    {
        var anyEmptySpace = Slots.Cast<string>().Any(it => it == null);
        IsDraw = !anyEmptySpace;
    }

    return winnerSymbol;
}

🧔 เย่ผ่าน สุดท้ายเราจะเห็นว่าเทสที่ 9 มันทำให้เราต้องกลับไปแก้เทสเคสทุกตัวเพื่อเช็คว่าเกมมันจะต้องไม่เสมอนะ ดังนั้นฝากไปลองเล่นกันต่อดูนะครับ เย่ๆๆ หนีจากบทความอันแสนยาวนี้ได้แล้ว

🎯 บทสรุป

การทำ Test First Design หรือ TDD สิ่งที่เราได้คือคุณภาพของโค้ดที่ดีเพราะโค้ดเราจะถูกคลุมด้วยเทสเคสทั้งหมดแล้ว ทำให้เรามั่นใจได้ว่าถ้าเกิดเหตุการณ์ที่อยู่ในเคสเกิดขึ้น โปรแกรมมันสามารถทำงานได้แบบไม่มี bug ค่อนข้างแน่นอน และเรายังสามารถทำ Refactor เพื่อทำ Clean Code เมื่อไหร่ก็ได้อีกด้วย และโค้ดที่ได้ออกมาก็จะไม่มีการ ออกแบบที่เกินความจำเป็น เพราะทุกอย่างเป็น minimal หมดเลย ซึ่งของที่เป็น minimal นี่แหละสามารถแก้ไขหรือปรับไปเป็นโครงสร้างอื่นๆได้ง่ายที่สุด

👨‍🚀 หัวใจหลักในการทำ TDD

หัวใจในการทำ Test-Driven Development คือการทำ 3 เรื่องครับ Red - Green - Refactor หรือพูดง่ายๆคือเขียนเทสให้มันไม่ผ่านก่อน แล้วทำให้มันผ่าน สุดท้ายค่อยกลับมาทำให้โค้ดมัน Clean ขึ้น นั่นเอง ซึ่งพอทำแบบนี้โค้ดที่เราเขียนมันก็จะค่อยๆเก่งขึ้นไปเรื่อยๆ ข้อผิดพลาดก็จะน้อยลงไปเรื่อยๆเช่นกันครับ

ต้องขอปรบมือให้เลยสำหรับคนที่อ่านตั้งแต่ต้นจนถึงตรงนี้ได้ สุดยอดกับความรักในการเขียนโค้ดจริงๆครับ

👨‍🚀 ดาวโหลดตัวอย่างทั้งหมด

👨‍🚀 คอร์สเรื่องการทำ TDD แบบเต็มสูบ

โหลดได้จาก GitHub นี้เบย

👦
https://github.com/saladpuk/demo-test-first
👦Test-Driven Development
Class Diagram แบบคิดเร็วๆ ไม่ต้องไปนั่งวิเคราะห์มันหรอกอ่านต่อเลย