สร้าง Android Device Farm ด้วย ADB + FFmpeg: การทดสอบ UI อัตโนมัติบนอุปกรณ์ 10–50 เครื่อง

@ridark_eth
อังกฤษ1 วันที่ผ่านมา · 07 ก.ค. 2569
209K
104
10
22
175

TL;DR

คู่มือนี้จะแนะนำการสร้าง Python pipeline เพื่อติดตั้ง APK, ทำ UI testing และสร้างรายงานวิดีโอแบบอัตโนมัติบนอุปกรณ์ Android จำนวนมากโดยใช้ ADB และ FFmpeg พร้อมบทวิเคราะห์ความคุ้มค่าอย่างละเอียด

เมื่อแอปของคุณต้องทำงานบนสมาร์ทโฟนจริงหลายสิบรุ่น จากผู้ผลิตหลายราย เวอร์ชัน Android และความละเอียดหน้าจอที่แตกต่างกัน การทดสอบด้วยตนเองจะกลายเป็นฝันร้ายอย่างรวดเร็ว ด้านล่างนี้คือไปป์ไลน์ Python ที่จะค้นหาอุปกรณ์ที่เชื่อมต่อทั้งหมดได้เอง ติดตั้ง APK บนอุปกรณ์ทั้งหมดพร้อมกัน รันการทดสอบแบบมีเครื่องมือวัด บันทึกวิดีโอของแต่ละการรัน และนำคลิปเหล่านั้นมาต่อกันเป็นรายงานวิดีโอไฟล์เดียวด้วย FFmpeg

สแต็กทั้งหมด > Python + ADB + FFmpeg < เป็นเครื่องมือ QA มาตรฐาน ไม่มีอะไรวิเศษ แค่ทำให้งานซ้ำซากเป็นอัตโนมัติ

Ridark - inline image

สถาปัตยกรรมไปป์ไลน์

text
1adb devices ──► รายการ serial numbers
2
3
4ติดตั้ง APK (พร้อมกันบนทุกอุปกรณ์ ด้วย ThreadPoolExecutor)
5
6
7สำหรับแต่ละอุปกรณ์:
8 screenrecord (รันในเบื้องหลัง) → am instrument (รันทดสอบ) → หยุด + ดึงวิดีโอ
9
10
11FFmpeg: ซ้อน serial number บนแต่ละคลิป + ต่อคลิป ──► test_report.mp4

สิ่งที่คุณต้องมี

  • ADB (Android Debug Bridge) จาก Android Platform Tools -> สำหรับควบคุมอุปกรณ์
  • Python 3.10+ -> สำหรับการประสานงาน (ผมใช้ list[str], tuple[...] โดยไม่ต้อง from __future__)
  • FFmpeg -> สำหรับประมวลผลและประกอบวิดีโอ
  • อุปกรณ์ที่เปิด USB debugging และเชื่อมต่อผ่าน USB (หรือผ่าน Wi-Fi ด้วย adb tcpip)

หลักการหนึ่งที่ใช้ตลอดทั้งโค้ดคือ ผมส่งอาร์กิวเมนต์ให้ subprocess เป็น list และ ไม่ใช้ shell=True วิธีนี้ปลอดภัยกว่า (ไม่มีการแทรกโค้ดผ่านชื่อไฟล์) และไม่พังเมื่อมีช่องว่างหรืออักขระพิเศษในพาธ

1. การค้นหาอุปกรณ์

Ridark - inline image

adb devices จะแสดงอุปกรณ์ที่อยู่ในสถานะ unauthorized หรือ offline ด้วย เราจะเก็บเฉพาะอุปกรณ์ที่อยู่ในสถานะ device จริงๆ

python
1import subprocess
2
3def get_devices() -> list[str]:
4 """คืนค่า serial numbers ของอุปกรณ์ทั้งหมดที่อยู่ในสถานะ 'device'"""
5 out = subprocess.run(
6 ["adb", "devices"],
7 capture_output=True, text=True, check=True,
8 ).stdout
9
10 serials: list[str] = []
11 for line in out.splitlines()[1:]: # บรรทัดแรกคือหัวข้อ "List of devices"
12 line = line.strip()
13 if line.endswith("\tdevice"): # ตัด unauthorized / offline ทิ้ง
14 serials.append(line.split("\t")[0])
15 return serials

2. การติดตั้ง APK แบบขนาน

การติดตั้งบนอุปกรณ์ 50 เครื่องทีละเครื่องนั้นช้า เราจะกระจายงานไปยัง thread pool: แต่ละ adb install เป็นโพรเซสแยกกัน ดังนั้น thread จึงทำงานได้ดีที่นี่ (เรารอ I/O ไม่ได้ใช้ CPU หนัก)

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3def install_apk(serial: str, apk_path: str) -> tuple[str, bool, str]:
4 r = subprocess.run(
5 ["adb", "-s", serial, "install", "-r", "-g", apk_path],
6 capture_output=True, text=True,
7 )
8 ok = r.returncode == 0 and "Success" in r.stdout
9 return serial, ok, (r.stdout + r.stderr).strip()
10
11def install_on_all(apk_path: str, serials: list[str]) -> None:
12 with ThreadPoolExecutor(max_workers=len(serials) or 1) as pool:
13 futures = [pool.submit(install_apk, s, apk_path) for s in serials]
14 for f in as_completed(futures):
15 serial, ok, log = f.result()
16 print(f"[{'OK' if ok else 'FAIL'}] {serial}")
17 if not ok:
18 print(f" {log}")

Flags: -r -> ติดตั้งใหม่โดยเก็บข้อมูลเดิม, -g -> อนุญาตสิทธิ์รันไทม์ทั้งหมดทันที (สะดวกเพื่อให้การทดสอบไม่สะดุดกับป๊อปอัปขอสิทธิ์)

3. การรัน instrumented tests

am instrument จะรันการทดสอบ Espresso/JUnit บนอุปกรณ์ มันจะพิมพ์ OK เมื่อสำเร็จ และ FAILURES!!! เมื่อล้มเหลวไปยัง stdout -> นั่นคือวิธีที่เราจะ判断ผลลัพธ์

python
1def run_instrumented_tests(
2 serial: str,
3 package: str,
4 runner: str = "androidx.test.runner.AndroidJUnitRunner",
5) -> tuple[str, bool]:
6 r = subprocess.run(
7 ["adb", "-s", serial, "shell", "am", "instrument", "-w",
8 f"{package}/{runner}"],
9 capture_output=True, text=True,
10 )
11 ok = "FAILURES!!!" not in r.stdout and r.returncode == 0
12 return serial, ok

package คือ ID ของแพ็คเกจทดสอบ ซึ่งปกติคือ com.example.app.test

4. การบันทึกหน้าจอระหว่างการทดสอบ

screenrecord จะบันทึกวิดีโอลงบนอุปกรณ์โดยตรง ข้อจำกัดที่ควรจำ: จำกัดเวลาประมาณ 3 นาทีต่อไฟล์ และ ไม่มีเสียง เราจะเริ่มบันทึกในเบื้องหลัง รันการทดสอบ จากนั้นหยุดอย่างสะอาดและดึงไฟล์มายังโฮสต์

วิธีที่เชื่อถือได้มากที่สุดในการหยุดบันทึกคือไม่ใช้สัญญาณกับ adb ในเครื่อง แต่ใช้ pkill บนอุปกรณ์แทน -> วิธีนี้ screenrecord จะปิดคอนเทนเนอร์ MP4 อย่างถูกต้อง

python
1import time
2
3def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:
4 return subprocess.Popen(
5 ["adb", "-s", serial, "shell", "screenrecord", remote]
6 )
7
8def stop_recording(
9 serial: str,
10 proc: subprocess.Popen,
11 remote: str = "/sdcard/run.mp4",
12 local: str = "run.mp4",
13) -> None:
14 # SIGINT บนอุปกรณ์จะทำให้ screenrecord ปิดไฟล์อย่างถูกต้อง
15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])
16 proc.wait(timeout=10)
17 time.sleep(1) # ให้เวลาอุปกรณ์ปิดคอนเทนเนอร์
18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)

5. การประกอบรายงานวิดีโอด้วย FFmpeg

อุปกรณ์แต่ละรุ่นมีความละเอียดหน้าจอต่างกัน ดังนั้นคุณจึงไม่สามารถต่อคลิปด้วย -c copy ได้ เราจะปรับขนาดแต่ละคลิปให้เป็นรูปแบบเดียวกัน (1080×1920) และซ้อน serial number ผ่าน drawtext ไปพร้อมกัน หลังจากนั้นคลิปทั้งหมดจะเหมือนกัน และการประกอบขั้นสุดท้ายจะเป็นการต่อแบบเร็วโดยไม่ต้องเข้ารหัสใหม่

python
1def label_clip(src: str, dst: str, label: str) -> None:
2 """ปรับขนาดเป็น 1080x1920 และซ้อน label (serial number ของอุปกรณ์)"""
3 vf = (
4 "scale=1080:1920:force_original_aspect_ratio=decrease,"
5 "pad=1080:1920:(ow-iw)/2:(oh-ih)/2,"
6 f"drawtext=text='{label}':x=20:y=20:fontsize=42:"
7 "fontcolor=white:box=1:[email protected]"
8 )
9 subprocess.run(
10 ["ffmpeg", "-y", "-i", src, "-vf", vf,
11 "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", dst],
12 check=True,
13 )
14
15def concat_report(clips: list[str], out: str = "test_report.mp4") -> None:
16 with open("concat_list.txt", "w") as f:
17 for c in clips:
18 f.write(f"file '{c}'\n")
19 subprocess.run(
20 ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
21 "-i", "concat_list.txt", "-c", "copy", out],
22 check=True,
23 )

6. การรวมทุกอย่างเข้าด้วยกัน

python
1def main() -> None:
2 apk = "app-debug.apk"
3 package = "com.example.app.test"
4 runner = "androidx.test.runner.AndroidJUnitRunner"
5
6 serials = get_devices()
7 if not serials:
8 print("No devices found. Check USB and the output of 'adb devices'.")
9 return
10
11 print(f"Devices found: {len(serials)}")
12 install_on_all(apk, serials)
13
14 labeled: list[str] = []
15 for serial in serials:
16 proc = start_recording(serial)
17 _, ok = run_instrumented_tests(serial, package, runner)
18 stop_recording(serial, proc, local=f"{serial}.mp4")
19 print(f"[{'PASS' if ok else 'FAIL'}] tests on {serial}")
20
21 out = f"{serial}_labeled.mp4"
22 label_clip(f"{serial}.mp4", out, serial)
23 labeled.append(out)
24
25 concat_report(labeled, "test_report.mp4")
26 print("Done: test_report.mp4")
27
28if __name__ == "__main__":
29 main()

ในที่นี้ลูป "บันทึก + ทดสอบ" จะทำงานตามลำดับทีละอุปกรณ์ -> ทำให้อ่านง่ายขึ้น สำหรับฟาร์มจริง คุณอาจต้องการห่อบล็อกนี้ด้วย ThreadPoolExecutor เช่นกัน เพื่อให้ทดสอบอุปกรณ์ทั้งหมดพร้อมกัน ตรรกะจะเหมือนกับการติดตั้งในส่วนที่ 2

อย่าประดิษฐ์ล้อใหม่: เครื่องมือสำเร็จรูป

Ridark - inline image
  • scrcpy -> การสะท้อนหน้าจอและควบคุมอุปกรณ์แบบเรียลไทม์จาก PC ของคุณ ขาดไม่ได้เมื่อต้องดีบักการทดสอบที่ล้มเหลว
  • Appium / Espresso / UI Automator -> เฟรมเวิร์กการทดสอบ UI แบบเต็มรูปแบบ; am instrument ข้างต้นคือเอนจินของมัน
  • Gradle Managed Devices -> รันการทดสอบบนอีมูเลเตอร์โดยตรงจาก build โดยไม่ต้องจัดการ ADB ด้วยตนเอง
  • Firebase Test Lab / AWS Device Farm -> ฟาร์มอุปกรณ์จริงบนคลาวด์ หากคุณไม่อยากดูแลฮาร์ดแวร์เอง
  • GNU parallel -> หากคุณต้องการจัดการจาก bash แทน Python

เศรษฐศาสตร์: ต้นทุนและสิ่งที่ประหยัดได้

ระบบอัตโนมัติในการทดสอบไม่ใช่เรื่อง "การทำเงินจากอากาศ" -> แต่เป็นการตัดรายการค่าใช้จ่ายที่แพงที่สุดสองรายการ: ชั่วโมงการทำงานของคน และ นาทีการใช้งานคลาวด์ ด้านล่างคือการประมาณการในสามขนาดทั่วไป ตัวเลขเป็นเพียงตัวอย่างและขึ้นอยู่กับภูมิภาค ผู้จำหน่ายอุปกรณ์ และราคาของผู้ให้บริการ โปรดตรวจสอบอัตราปัจจุบันก่อนซื้อ

ฟาร์มของคุณเอง -> ลงทุนครั้งเดียว

รายการ

สำหรับ 10 อุปกรณ์

สำหรับ 30 อุปกรณ์

สำหรับ 50 อุปกรณ์

โทรศัพท์ Android มือสอง (~$60/เครื่อง)

~$600

~$1,800

~$3,000

ฮับ USB ที่มีไฟเลี้ยง

~$100

~$250

~$400

Mini-PC / โฮสต์

~$400

~$400

~$500

สายเคเบิล, ชั้นวาง, อุปกรณ์เบ็ดเตล็ด

~$80

~$150

~$250

รวมครั้งเดียว

~$1,200

~$2,600

~$4,150

ค่าไฟฟ้าต่อเดือน

เล็กน้อย

~$10–20

~$20–40

นี่คือรายจ่ายลงทุน: จ่ายครั้งเดียว แล้วฟาร์มก็ทำงานได้นานหลายปีโดยแทบไม่มีค่าใช้จ่าย

คลาวด์ -> จ่ายต่อนาที

Firebase Test Lab, AWS Device Farm, BrowserStack และอื่นๆ คิดค่าบริการต่อ device-minute โดยประมาณ $0.05–0.20 ต่อ device-minute การรัน regression ครั้งเดียวบน 30 อุปกรณ์ ครั้งละ 5 นาที เท่ากับ 150 device-minutes หรือประมาณ ~$7.5–30 ต่อการรัน

คูณด้วยความถี่ CI:

ความถี่ในการรัน

จำนวนรันต่อเดือน

ต้นทุน (ที่ ~$15/รัน)

2× ต่อวัน

~44

~$660/เดือน

10× ต่อวัน

~220

~$3,300/เดือน

ทุกครั้งที่ push (ทีมที่ทำงานหนัก)

500+

$7,500+/เดือน

จุดคุ้มทุน: ฟาร์ม 30 อุปกรณ์ (~$2,600) คุ้มทุนเมื่อเทียบกับคลาวด์ในประมาณ 4 เดือน ที่การรันวันละ 2 ครั้ง; กับ CI ที่ใช้งานหนัก คุ้มทุนในเวลาไม่ถึงเดือน หลังจากนั้นบิลคลาวด์ก็จะไหลทุกเดือน ในขณะที่ฟาร์มไม่มีค่าใช้จ่ายเพิ่ม

แรงงานคน -> สิ่งที่ถูกปลดปล่อย

การรัน regression scenario เดียวด้วยตนเองบน 30 อุปกรณ์ ใช้เวลาประมาณ หนึ่งวันทำงานของวิศวกร QA การรัน regression สัปดาห์ละสองครั้งรวมเป็น ~8 วันคนต่อเดือน ด้วยต้นทุน QA ประมาณ $1,600–2,700/เดือน นั่นคือส่วนสำคัญของเงินเดือนที่ไปป์ไลน์นี้จะปลดปล่อยให้ไปทำงานที่มีความหมาย แทนที่จะเป็นงาน "เชื่อมต่อ–ติดตั้ง–กด–บันทึก" ซ้ำๆ ×30

สิ่งนี้แปลงเป็นเงินได้อย่างไร

ไม่มีรายได้โดยตรง "จากสคริปต์" ที่นี่ -> แต่มีกลไกทางอ้อมสามอย่างที่จับต้องได้มาก:

  • ปล่อยฟีเจอร์ได้เร็วขึ้น Regression เสร็จในไม่กี่นาทีแทนที่จะเป็นวัน → คุณส่งฟีเจอร์ได้บ่อยขึ้น → คุณตอบสนองต่อตลาดได้เร็วขึ้น สำหรับผลิตภัณฑ์แบบสมัครสมาชิก สิ่งนี้เชื่อมโยงโดยตรงกับการรักษาลูกค้าและรายได้
  • บั๊กในโปรดักชั่นน้อยลง การจับ crash บน Samsung รุ่นใดรุ่นหนึ่งก่อนปล่อยมีต้นทุนเพียงเล็กน้อย; crash เดียวกันที่ถึงมือผู้ใช้หมายถึงคะแนนร้านค้าที่ลดลง การสูญเสียลูกค้า และการขอเงินคืน ทุกบั๊กที่ถูกจับได้ตั้งแต่เนิ่นๆ คือรีวิวเชิงลบจำนวนหนึ่งที่จะไม่มีวันถูกเขียน
  • ขายเป็นบริการได้ การตั้งค่าฟาร์มอุปกรณ์และการทดสอบ CI เป็นบทบาทที่เป็นที่ต้องการในงานฟรีแลนซ์และเอาท์ซอร์ส ไปป์ไลน์ข้างต้นคือแกนหลักที่พร้อมใช้งานสำหรับข้อเสนอดังกล่าว

ความแตกต่างที่สำคัญจาก "แผนการสร้าง engagement": ที่นั่น เงินมาจากการหลอกลวงอัลกอริทึมและจบลงด้วยการแบน ที่นี่ เงินมาจากชั่วโมงที่ประหยัดได้และการป้องกันความเสียหาย แบบแรกพังทลาย; แบบหลังเป็นกรณีธุรกิจที่ยั่งยืนซึ่งคุณไม่ต้องอายที่จะนำเสนอให้ลูกค้าดู

สรุป

ผลลัพธ์คือไปป์ไลน์ที่ทำซ้ำได้: คำสั่งเดียว และแอปจะถูกทดสอบบนอุปกรณ์ทั้ง fleet พร้อมรายงานวิดีโอที่แสดงพฤติกรรมบนอุปกรณ์แต่ละเครื่อง

บุ๊กมาร์กและติดตามเพื่อไม่พลาดบทความที่มีประโยชน์ที่จะช่วยประหยัดเงิน 📝

บันทึกในคลิกเดียว

อ่านบทความไวรัลเชิงลึกด้วย AI ใน YouMind

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
สำหรับครีเอเตอร์

เปลี่ยน Markdown ของคุณให้เป็นบทความ 𝕏 ที่สะอาดตา

เวลาคุณเผยแพร่งานเขียนยาวของตัวเอง การจัดรูปแบบรูปภาพ ตาราง และบล็อกโค้ดให้เข้ากับ 𝕏 นั้นน่าปวดหัว YouMind เปลี่ยนร่าง Markdown ทั้งฉบับให้เป็นบทความ 𝕏 ที่สะอาดตาและพร้อมโพสต์ทันที

ลอง Markdown เป็น 𝕏

แพตเทิร์นให้ถอดรหัสเพิ่มเติม

บทความไวรัลล่าสุด

สำรวจบทความไวรัลเพิ่มเติม