← Back to notes
TECHNICAL FIELD NOTES

Test Double in Swift

อธิบาย Test Double ทั้ง Mocks, Stubs, Dummy, Fake และ Spies ผ่านตัวอย่างแอพจัดการสต๊อกและการเขียนเทสด้วย Swift

Previously published at Original on the previous platform ↗ · First published

หมายเหตุการเก็บบทความ (2026-10-18): บทความนี้คงตัวอย่างและบริบท Xcode 12.3 / Swift 5.3.1 ตามต้นฉบับ ไม่ใช่คำแนะนำการติดตั้งในปัจจุบัน มีเพียงการซ่อมรูปแบบโค้ดและข้อความ ไม่ได้รันตัวอย่าง โค้ดทดสอบเป็นส่วนย่อยที่ต้องอาศัยบริบท XCTest ในโปรเจกต์เดิม และยังคง init? ของ PowderPuffStore ตามบทความ แม้ตัวอย่างใน repository จะใช้ required init? จึงไม่ควรถือว่าตัวอย่างที่ตัดมานี้ผ่านการคอมไพล์แล้ว

Test Double คืออะไร? เอาไว้ทำอะไร?

ภาพประกอบ

Photo by JESHOOTS.COM on Unsplash

ก่อนอื่นต้องขอบอกเลยว่าห่างหายจากการเขียนบทความไปนานมาก ได้แต่อ่านๆนู่นนี่ แล้วก็เก็บเข้ากรุไป ไม่รู้จะเรียบเรียงอะไรมาเขียนดี แต่พอดีนึกถึงคำถามที่น้องคนนึงถามผมไว้เมื่อเดือนที่แล้วออก น้องถามผมว่า…

Mocks กับ Stubs นี่ต่างกันยังไง?

ด้วยความสัตย์จริง คือตอบไม่ได้!!! ตอนนั้นข้อมูลในหัววิ่งตีกันไปหมด Mocks, Stubs, Dummy, Fake วิ่งสวนกันไปมา แบบเห้ย ที่เราเขียนๆเทสไว้ตอนนั้นตอนนี้สรุปมันเรียกว่าไรนะ อันไหน Stubs อันไหน Mocks เราก็เคยเขียนเทสมานี่หว่า เอาจริงๆแล้วมันต่างกันยังไงนะ ซึ่งสุดท้ายแล้วผมก็ตอบน้องว่ามันแตกต่างกันยังไงไม่ได้ 😢

แต่ด้วยความที่มันค้างในหัวมาละ ไม่ได้ไม่ได้… เราต้องหาคำตอบสิ ไม่งั้นคำถามมันจะวนเวียนในหัวตามหลอกหลอนเราทุกภพทุกชาติไป … มา มาเริ่มกัน 👻

Test Double คืออะไร? ⭐️

Test Double is a generic term for any case where you replace a production object for testing purposes.

ก่อนไปหาความหมายของ Mocks & Stubs เราควรเข้าใจแม่แบบของมันก่อน นั่นคือ Test Double นั่นเอง จากข้อความด้านบน ผมลอกมาจากเว็บลุง Martin Fowler ซึ่งลุง Gerrard Meszaros คนที่เขียนหนังสือ xUnit Test Patterns เป็นคนนิยาม Test Double ขึ้นมา(เพราะเห็นว่าลุงแกให้นิยามไว้สั้นและกระชับดี) เค้าบอกว่า…(ซึ่งผมแปลเอง 55+)

Test Double คือ อะไรก็ตามหรือ Object ที่เราสร้างมันขึ้นมา เพื่อให้ทดแทนการทำงานจริงๆในกระบวนการที่เราต้องการทดสอบการทำงานนั้นๆ

เรายกตัวอย่างคล้ายๆกับ Stun Double ที่มีในวงการภาพยนต์ เช่นฉากเสี่ยงๆ ขี่มอไซต์ยกล้อ ตีลังกาหลบระเบิด เขาจะใช้ “ตัวแสดงแทน (สตั้นแมน)” แทนการให้ “พระเอกสุดหล่อของเรา” ไปยกล้อ ไปหลบระเบิดเอง

พอมองกลับมาที่ฝั่ง Software อีกรอบ มันก็จะคล้ายกับที่เรา “สร้างตัวหลอก” เพื่อมาทำงานแทนในส่วนของ การต่อ API, การต่อ Database หรือส่วนที่ต้องต่อ Dependencies อื่นๆ ลองนึกภาพตามง่ายๆว่า ถ้าอย่างเช่นเราต้องการทำแอพการเงินตัวนึง ที่สามารถโอนเงินไปมาระหว่างผู้ใช้ได้ โดยต้องผ่าน API หรือต้องบันทึกลงบน Database หากเราไม่ทำ Test Double แต่เราต่อไปที่ Production โดยตรงมันจะสยองแค่ไหน Database บน Production ของเราก็จะบวมไปด้วยข้อมูลที่เราใช้ทดสอบ หรือบางจังหวะที่ทีมงานพัฒนา API มีการปรับปรุงแก้ไข เราก็จะเทสไม่ได้อีก กลายเป็นดราม่าแบบ ก็ทีม API ทีม DB ทำไม่เสร็จผมก็ทำแอพต่อไม่ได้สิ… อยากบอกว่ามันไม่เกี่ยวว้อยยยยยยยยยยยยยยยยยยยยยยย ซึ่ง Test Double นี่แหละ จะมาช่วยแก้ปัญหาตรงนี้ แฮปปี้กันทุกฝ่าย

  • Mobile Team: ไม่ต้องรอ API เสร็จ ลุยได้เลย (แต่อย่าลืมคุยกันเรื่อง Request & Reponse หรืออื่นๆ กับทีม Backend ก่อนนะ)
  • Backend Team: ทำ API สวยๆได้อย่างสบายใจ ไม่ต้องห่วงว่า Mobile Team จะมาสะกิด… เสร็จรึยัง เสร็จรึยังเอ่ย
  • Business(include CEO) Team: ไหนขอดูแอพหน่อย… Mobile Team เราก็โชว์หล่อๆให้เค้าดูได้เลย โดยที่ไม่ต้องรอ API Team

แค่นี้ก็ฟินละ เพราะการ “รอ” มันมี “มูลค่า” ของมัน แต่เราก็ต้องเข้าใจเรื่องจริงที่สุดอีกข้อหนึ่งเหมือนกันว่า สุดท้ายแล้ว เราก็ยังต้องขายของที่อยู่บน Production อยู่ดี แต่ถือว่า Test Double นั้นช่วยเราประหยัดเวลาในการ “รอ” มากขึ้นจริงๆ

Test Double มีอะไรบ้าง? 😃

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

Test Double นั้นมีอยู่ 5 อย่าง แต่ผมขอเรียงจากที่เราใช้กันบ่อยๆก่อนนะครับ

  • Mocks
  • Stubs
  • Dummy
  • Fake
  • Spies

มาลุยกัน ✋

เตรียมของกันก่อน เราจะลองจำลองแอพขึ้นมาตัวหนึ่ง เอาเป็น แอพเช็คสต๊อกแป้งเด็ก ฟิลแบบลุงข้างบ้านกลับมาจากออสแล้วมาจ้างเราทำแอพเล็กๆให้ เอาแบบฟีเจอร์กุ๊กกิ๊กพอประมาณ

  • เช็คยอดแป้งในสต๊อก
  • เพิ่ม ลด แป้งในสต๊อก
  • ส่งข้อความผ่านเข้าไลน์ บอกผู้ค้าปลีกได้

เดี๋ยวเรามาลองทำไปพร้อมกับอธิบายแต่ละส่วนของ Test Double ไปด้วยกันครับ แต่ก่อนอื่นขอวางโครง แอพขายแป้งเด็ก ง่ายๆขึ้นมาก่อน

อุปกรณ์ที่ต้องเตรียมจะมี 🎮

  • Tool: Xcode 12.3 (12C33)
  • Language: Swift 5.3.1
// Main protocols
protocol APIService {
    func getPowderInStock() -> Result<[Powder], Error>
}

protocol MessengerService {
    func sendMessage(to id: String) -> Result<Bool, Error>
}

protocol GoodsStore {
    var apiService: APIService { get }
    var lineService: MessengerService { get }
    var stocks: [Powder] { get set }

    init?(_ apiService: APIService, lineService: MessengerService)

    func getPowderInStocks() -> [Powder]
    func addPowder(_ powder: Powder)
    func removePowder() -> Powder?
    func sendMessage(to lineID: String)
}

// Class that conform these protocols above for use in production
class PowderAPIService: APIService {
    func getPowderInStock() -> Result<[Powder], Error> {
        // Assume these data below will return from API
        let powder1 = Powder(grade: .smooth, price: 100)
        let powder2 = Powder(grade: .strong, price: 200)
        let powder3 = Powder(grade: .invisible, price: 300)

        return .success([powder1, powder2, powder3])
    }
}

class LineMessengerService: MessengerService {
    func sendMessage(to groupID: String) -> Result<Bool, Error> {
        // Call some api for send message
        return .success(true)
    }
}

ฝั่ง Data Model และ Error เราก็เตรียมๆไว้ประมาณนี้

enum Grade {
case smooth
case strong
case invisible
}

struct Powder {
    let grade: Grade
    let price: Int
}

enum PowderStoreError: Error {
case apiServiceIsUnavailable
case lineServiceIsUnavailable
}

ส่วนของคลังเก็บแป้งเราก็จะหน้าตาเอาเป็นประมาณนี้

class PowderPuffStore: GoodsStore {
    let apiService: APIService
    let lineService: MessengerService
    var stocks: [Powder] = []

    init?(_ apiService: APIService, lineService: MessengerService) {
        self.apiService = apiService
        self.lineService = lineService

        let getStocksResult = apiService.getPowderInStock()
        switch getStocksResult {
        case .success(let stocks):
            self.stocks = stocks
        case .failure(let error):
            // Handle error here
            return nil
        }
    }

    func getPowderInStocks() -> [Powder] {
        return stocks
    }

    func addPowder(_ powder: Powder) {
        stocks.append(powder)
    }

    func removePowder() -> Powder? {
        return stocks.removeFirst()
    }

    func sendMessage(to lineID: String) {
        let sendMessageResult = lineService.sendMessage(to: lineID)
        switch sendMessageResult {
        case .success(_):
            print("Send message to lineID: \(lineID) success")
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}

เดี๋ยวเรามาลองเริ่มรู้จักไปทีละส่วนแล้วก็เขียนเทสในแต่ละแบบดูครับ
Let’s go 🚀

ภาพประกอบ

Photo by Tanaphong Toochinda on Unsplash

Mocks คือ?

Object ที่ใช้สำหรับทดสอบ “พฤติกรรม” การทำงาน(Behavior Verification) ของ Method หรือ Class ของเราโดยที่ไม่ต้องสนใจผลลัพท์ของการทำงาน แต่สนใจว่า มันทำงานตามที่เราเรียกใช้หรือไม่

ตัวอย่างของ Mocks

จากความหมายด้านบน เราจะลองเขียนเทสในแบบ Mocks ดู ว่าถ้าเราเรียกใช้ Method/Class จะทำงานตามที่เราเรียกใช้ไหม คือเราจะไม่สนใจผลลัพท์ที่เราจะได้กลับออกมาจาก Method/Class ที่เราเรียกใช้นะ เราแค่อยากรู้ว่ามันทำงานไหม เป็นยังไงจากการที่เราเรียกใช้มัน

// Mocks
class MockPowderAPIService: APIService {
    var checkGetPowderInStockeWasCalled = false

    func getPowderInStock() -> Result<[Powder], Error> {
        checkGetPowderInStockeWasCalled = true
        return .success([])
    }
}

class MockLineMessengerService: MessengerService {
    var checkSendMessageWasCalled = false
    var lineID = ""

    func sendMessage(to id: String) -> Result<Bool, Error> {
        checkSendMessageWasCalled = true
        lineID = id
        return .success(true)
    }
}

func testGetPowderInStockWasCalledWhenInitialSuccess() {
    let apiService = MockPowderAPIService()
    let lineService = MockLineMessengerService()
    let _ = PowderPuffStore(apiService, lineService: lineService)

    XCTAssertTrue(apiService.checkGetPowderInStockeWasCalled)
}

func testLineMessengerServiceSendMessageWasCalled() {
    let apiService = MockPowderAPIService()
    let lineService = MockLineMessengerService()
    let powderPuffStore = PowderPuffStore(apiService, lineService: lineService)

    let lineID = "igroomgrim"
    powderPuffStore?.sendMessage(to: lineID)

    XCTAssertTrue(lineService.checkSendMessageWasCalled)
    XCTAssertEqual(lineService.lineID, lineID)
}

Stubs คือ?

Object ที่ใช้สำหรับทดสอบ “สถานะ” การทำงาน(State Verification) โดยที่เราได้เตรียมผลลัพท์ของการทำงานนี้ไว้ เพื่อดูว่าเราได้ “ผลลัพท์” ตามที่เราต้องการไหม หรือเราจะเอา Stub มาใช้ในกับการทดสอบสถานะที่เกิดขึ้นได้ยากๆ เช่น API เจ้ง, Network error ว่าผลลัพท์นั้นเป็นตามที่เราเตรียมไว้ไหม

ตัวอย่างของ Stubs

จากตัวอย่างด้านล่าง เราอยากทดสอบว่า ถ้าเกิด API ที่เราเอาไว้ดึงแป้งในสต๊อกหลักพัง ตัว Store ของเราควรจะยังเปิดทำงานไม่ได้ด้วยเช่นกัน

// Stubs
class StubPowderAPIService: APIService {
    func getPowderInStock() -> Result<[Powder], Error> {
        return .failure(PowderStoreError.apiServiceIsUnavailable)
    }
}

class StubLineMessengerService: MessengerService {
    func sendMessage(to id: String) -> Result<Bool, Error> {
        return .success(true)
    }
}

func testPowerPuffStoreInitailFailed() {
    let apiService = StubPowderAPIService()
    let lineService = StubLineMessengerService()
    let powderPuffStore = PowderPuffStore(apiService, lineService: lineService)

    XCTAssertNil(powderPuffStore)
}

Dummy คือ?

Object ที่เอาไว้ใช้สำหรับ “ทดสอบ” อย่างเดียว ทำหน้าที่เป็น placeholder ไว้ให้ดูหลอกๆ ไม่ต้องมี Implementation อะไรเลย อาจจะแค่ Object หรือ Method ที่ return แค่ true, false โต้งๆ แต่ว่าพอเอามาทดสอบก็สามารถทำงานได้ปกติ แต่อย่าเอาไปใช้จริงๆหละ

ตัวอย่างของ Dummy

เราสร้าง Dummy ไว้สำหรับทดสอบห้วนๆเลยอย่างเช่นถ้าการเรียกใช้ getPowderInStock มันต้องมีการเรียกหรือเชื่อมต่อกับ API ที่เป็น Dependency จริงที่อยู่ด้านนอก แต่เราแค่อยากทดสอบง่ายๆว่า ถ้าเราจะลองสร้าง PowerPuffStore ขึ้นมา มันสามารถสร้างได้ไหม แค่นั้นเลย ลดเรื่องการต่อ Dependency ได้ด้วย

// Dummy
class DummyPowderAPIService: APIService {
    func getPowderInStock() -> Result<[Powder], Error> {
        return .success([])
    }
}

class DummyLineMessengerService: MessengerService {
    func sendMessage(to id: String) -> Result<Bool, Error> {
        return .failure(PowderStoreError.lineServiceIsUnavailable)
    }
}

func testPowderPuffStoreInitialSuccess() {
    let apiService = DummyPowderAPIService()
    let lineService = DummyLineMessengerService()
    let powderPuffStore = PowderPuffStore(apiService, lineService: lineService)

    XCTAssertNotNil(powderPuffStore)
}

Fake คือ?

Object ที่เอาไว้ใช้สำหรับ “จำลอง” การทำงานหรือผลลัพท์ของ Class/Method ที่เราต้องการทดสอบ เพื่อเอาไว้แทนของจริงหรือลดความซับซ้อน

ตัวอย่างของ Fake

มันจะมีหลายๆกรณีที่เราทำบน Production ตรงๆไม่ได้ แต่เราอยากได้การทดสอบที่เหมือนที่สุด บางทีเหมือนจนเราก็งงว่านี่เราเขียนเทสที่ทำงานได้สมจริงขนาดนี้เชียวเหรอ ในตัวอย่างด้านล่าง เราสร้าง “Store หลอก” ขึ้นมาตัวนึง เพื่อที่จะไม่ต้องไปยุ่งกับสต๊อกแป้งใน “Store จริง” (สมมติว่า Stock นั้นมันติดพ่วงกับ Database อยู่ เราก็ไม่ต้องไปกังวลว่าตอนเราทดสอบ Database เราจะบวมไปด้วยข้อมูลที่ใช้ในการทดสอบ จะเพิ่มลดทดสอบแค่ไหนก็ทำได้อย่างสะดวกขึ้น) ตรงนี้เราทำให้เป็น Fake ได้ทั้ง Method/Class เล็กใหญ่ ตามเป้าที่เราอยากทดสอบได้นะครับ

// Fake
class FakePowderAPIService: APIService {
    func getPowderInStock() -> Result<[Powder], Error> {
        let powder = Powder(grade: .smooth, price: 100)
        return .success([powder])
    }
}

class FakePowerPuffStore: GoodsStore {
    var apiService: APIService
    var lineService: MessengerService
    var stocks: [Powder]

    required init?(_ apiService: APIService, lineService: MessengerService) {
        self.apiService = apiService
        self.lineService = lineService

        let getStocksResult = apiService.getPowderInStock()
        switch getStocksResult {
        case .success(let stocks):
            self.stocks = stocks
        case .failure(let error):
            // Handle error here
            return nil
        }
    }

    func getPowderInStocks() -> [Powder] {
        return stocks
    }

    func addPowder(_ powder: Powder) {
        stocks.append(powder)
    }

    func removePowder() -> Powder? {
        return stocks.removeFirst()
    }

    func sendMessage(to lineID: String) {
        let sendMessageResult = lineService.sendMessage(to: lineID)
        switch sendMessageResult {
        case .success(_):
            print("Send message to lineID: \(lineID) success")
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}

func testAddAndRemovePowderInStore() {
    let apiService = FakePowderAPIService()
    let lineService = SpyLineMessengerService()
    let powderPuffStore = FakePowerPuffStore(apiService, lineService: lineService)

    // Check stock count after initial (Fake)PowerPuffStore
    XCTAssertEqual(powderPuffStore?.getPowderInStocks().count, 1)

    // Add 3 powder to store
    powderPuffStore?.addPowder(Powder(grade: .smooth, price: 100))
    powderPuffStore?.addPowder(Powder(grade: .strong, price: 200))
    powderPuffStore?.addPowder(Powder(grade: .invisible, price: 300))

    // Check stock count after add 3 powder to store, powder in store count must be 4 in stock
    XCTAssertEqual(powderPuffStore?.getPowderInStocks().count, 4)

    // Remove 2 powder from store
    _ = powderPuffStore?.removePowder()
    _ = powderPuffStore?.removePowder()

    // Check stock count after remove 2 powder to store, powder in store count must be 2 in stock
    XCTAssertEqual(powderPuffStore?.getPowderInStocks().count, 2)
}

Spies คือ?

Object ที่เอาไว้สำหรับ “สอดแนม” การทำงานว่าทำงานตามจำนวนครั้งที่เรียกถูกต้องหรือไม่ ซึ่งมันจะมีความคล้ายกับ Mock พอสมควรเลยทีเดียว แต่ Spies จะสนใจแค่ว่า Class/Method ที่เราเรียกใช้นั้น ถูกต้องตามจำนวนครั้งที่เราเรียกใช้จริงๆไหม

ตัวอย่างของ Spies

การทดสอบจะตามชื่อเค้าเลย เพราะเค้าคือหน่วยสายลับ สอดแนม ที่จะดูว่า การทำงานของ Class/Method ที่เราเรียกใช้นั้น ทำงานตามจำนวนครั้งที่มันถูกเรียกใช้จริงๆไหม อย่างเช่นเราเรียก sendMessage 2 ครั้ง มันก็ควรจะทำงาน 2 ครั้งจริงๆ

// Spies
class SpyPowderAPIService: APIService {
    var checkGetPowderInStockWasCalledCount = 0

    func getPowderInStock() -> Result<[Powder], Error> {
        checkGetPowderInStockWasCalledCount += 1
        return .success([])
    }
}

class SpyLineMessengerService: MessengerService {
    var checkSendMessageWasCalledCount = 0

    func sendMessage(to id: String) -> Result<Bool, Error> {
        checkSendMessageWasCalledCount += 1
        return .success(true)
    }
}

func testGetPowderInStockWasCalledOnceWhenInitialSuccess() {
    let apiService = SpyPowderAPIService()
    let lineService = SpyLineMessengerService()
    let _ = PowderPuffStore(apiService, lineService: lineService)

    XCTAssertEqual(apiService.checkGetPowderInStockWasCalledCount, 1)
}

func testLineMessengerServiceSendMessage() {
    let apiService = SpyPowderAPIService()
    let lineService = SpyLineMessengerService()
    let powderPuffStore = PowderPuffStore(apiService, lineService: lineService)

    powderPuffStore?.sendMessage(to: "1112")
    powderPuffStore?.sendMessage(to: "1234")

    XCTAssertEqual(lineService.checkSendMessageWasCalledCount, 2)
}

ภาพประกอบ

Photo by Hannah Tasker on Unsplash

สรุป 📖

เราได้เรียนรู้ของความหมายและวิธีการเขียนเทสคร่าวๆ ครบทั้ง 5 แบบสำหรับ Test Double ทั้ง Mocks, Stubs, Fake, Dummy, Spies เราจะพบว่าจากประสบการณ์ที่เราอาจเคยเขียน Unit Test มานั้น มันคุ้นตาสุดๆ เพียงแค่บางทีเราอาจแยกไม่ออกหรือบางครั้งตอนเราเขียนอาจจะผสมๆกันไป แต่เป้าหมายของเราในการเขียนเทสนั้นก็เพื่อทดสอบว่าการทำงานของ Class/Method ที่เราสร้างขึ้นมาใช้ทำงานนั้น ทำงานได้ตามเป้าหมาย ถูกต้องหรือไม่ ซึ่งยิ่งเราศึกษาลึกเพิ่มลงไปแล้ว ยิ่งจะพบว่าเราควรต้องเรียนรู้เพิ่มทั้งเรื่อง Unit-Test, Integration Test, Functional Test, TDD, Dependency-Injection บลาๆ รวมทั้งการเขียนโปรแกรมแบบ Object-Oriented, SOLID principle และอื่นๆที่จะช่วยให้เราเขียนเทสได้ไม่ยาก เพราะหากเราเขียนเทสแล้วรู้สึกยาก แปลว่าเราอาจจะออกแบบ Class/Method นั้นมาไม่ดี ยุ่งเหยิง หรือทำงานซับซ้อนหลายน่าที่เกินไปครับ

สุดท้าย ท้ายสุด 📝

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

แล้วก็ขอขอบคุณข้อมูลจากบทความดีๆและอ้างอิงจาก (ตามไปอ่านของพี่ๆในนี้อาจรู้เรื่องกว่าที่ผมเขียนก็เป็นได้ครับ 5555+) จริงๆมีเยอะกว่านี้ผมแปะไว้ไม่หมดครับ ขอบคุณบทความเหล่านี้มากๆครับ
เว็บลุง Martin Fowler, MocksArentStubs, SwiftSenpai
บล๊อกพี่ Somkiat และพี่ iFew

  • Test Double in Swift — Example source code here

ก่อนจากกัน…ฝากกดไลท์ กดแชร์เพจโปรแกรมมิ่งสนุกๆไว้ให้ติดตามผ่านทาง Facebook ไว้เรียนรู้ไปด้วยกันครับ 🙇
https://www.facebook.com/igroomgrimonthefloor/

อยากให้ทุกคนสนุกกับการเขียนโค้ด
Be lazy & Happy Coding 🤤🖥🤗

Read more notes →