ตัวอย่างการทดสอบ TF ตั้งแต่ต้นจนจบ

บทแนะนำนี้จะแนะนำการสร้างการกำหนดค่าการทดสอบ "Hello World" ของ Trade Federation (Tradefed หรือ TF) และแนะนำการใช้งานเฟรมเวิร์ก TF คุณจะเริ่มต้นจากสภาพแวดล้อมการพัฒนาเพื่อสร้างการกำหนดค่าง่ายๆ และเพิ่มฟีเจอร์

บทแนะนําจะแสดงกระบวนการพัฒนาการทดสอบเป็นชุดแบบฝึกหัด ซึ่งแต่ละแบบฝึกหัดประกอบด้วยหลายขั้นตอนที่สาธิตวิธีสร้างและปรับแต่งการกําหนดค่าทีละน้อย เรามีตัวอย่างโค้ดทั้งหมดที่จําเป็นในการกําหนดค่าการทดสอบให้เสร็จสมบูรณ์ และชื่อของแบบฝึกหัดแต่ละรายการจะมีคำอธิบายประกอบด้วยตัวอักษรที่อธิบายบทบาทที่เกี่ยวข้องในขั้นตอนนั้นๆ

  • D สำหรับนักพัฒนาแอป
  • I สำหรับผู้ผสานรวม
  • R สำหรับ Test Runner

หลังจากทําตามบทแนะนําเสร็จแล้ว คุณจะมีการกำหนดค่า TF ที่ใช้งานได้และเข้าใจแนวคิดสําคัญหลายประการในเฟรมเวิร์ก TF

ตั้งค่าสหพันธ์การค้า

โปรดดูรายละเอียดเกี่ยวกับการตั้งค่าสภาพแวดล้อมการพัฒนา TF ที่หัวข้อการตั้งค่าเครื่อง ส่วนที่เหลือของบทแนะนำนี้จะถือว่าคุณเปิดเชลล์ที่เริ่มต้นค่าเป็นสภาพแวดล้อม TF แล้ว

บทแนะนํานี้จะแสดงการใส่การกําหนดค่าและคลาสของการกำหนดค่าลงในคลังหลักของเฟรมเวิร์ก TF เพื่อให้เข้าใจง่าย ซึ่งสามารถขยายการทำงานเพื่อพัฒนาโมดูลนอกสคีมาซอร์สโค้ดได้โดยคอมไพล์ JAR ของ Tradefed จากนั้นคอมไพล์โมดูลกับ JAR นั้น

สร้างชั้นเรียนทดสอบ (D)

มาสร้างการทดสอบ Hello World ที่แสดงข้อความไปยัง stdout กัน โดยทั่วไปแล้ว การทดสอบ Tradefed จะใช้อินเทอร์เฟซ IRemoteTest ต่อไปนี้เป็นการใช้งาน HelloWorldTest

package com.android.tradefed.example;

import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IRemoteTest;

public class HelloWorldTest implements IRemoteTest {
    @Override
    public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
        CLog.i("Hello, TF World!");
    }
}

บันทึกโค้ดตัวอย่างนี้ลงใน <tree>/tools/tradefederation/core/src/com/android/tradefed/example/HelloWorldTest.java และสร้าง tradefed อีกครั้งจากเชลล์

m -jN

โปรดทราบว่า CLog.i ในตัวอย่างด้านบนใช้เพื่อส่งเอาต์พุตไปยังคอนโซล ดูข้อมูลเพิ่มเติมเกี่ยวกับการบันทึกใน Trade Federation ได้ที่การบันทึก (D, I, R)

หากบิลด์ไม่สำเร็จ โปรดดูหัวข้อการตั้งค่าเครื่องเพื่อให้แน่ใจว่าคุณไม่ได้พลาดขั้นตอนใดไป

สร้างการกำหนดค่า (I)

การทดสอบของ Trade Federation จะทําให้เรียกใช้ได้โดยการสร้างการกําหนดค่า ซึ่งเป็นไฟล์ XML ที่บอกให้ TradeFed ทราบการทดสอบ (หรือการทดสอบ) ที่จะเรียกใช้ รวมถึงโมดูลอื่นๆ ที่จะเรียกใช้และลําดับการเรียกใช้

มาสร้างการกําหนดค่าใหม่สําหรับ HelloWorldTest (โปรดสังเกตชื่อคลาสแบบเต็มของ HelloWorldTest)

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
</configuration>

บันทึกข้อมูลนี้ลงในไฟล์ helloworld.xml ได้ทุกที่ในระบบไฟล์ในเครื่อง (เช่น /tmp/helloworld.xml) TF จะแยกวิเคราะห์ไฟล์ XML การกำหนดค่า (หรือที่เรียกว่า config) โหลดคลาสที่ระบุโดยใช้การสะท้อน สร้างอินสแตนซ์ แคสต์เป็น IRemoteTest และเรียกใช้เมธอด run

เรียกใช้การกําหนดค่า (R)

จากเชลล์ ให้เปิดคอนโซล TradeFed โดยทำดังนี้

tradefed.sh

ตรวจสอบว่าอุปกรณ์เชื่อมต่อกับเครื่องโฮสต์และแสดงใน TradeFed

tf> list devices
Serial            State      Product   Variant   Build   Battery
004ad9880810a548  Available  mako      mako      JDQ39   100

คุณสามารถเรียกใช้การกําหนดค่าได้โดยใช้คําสั่ง run <config>console โดยคุณสามารถลองทำสิ่งต่อไปนี้

tf> run /tmp/helloworld.xml
05-12 13:19:36 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World!

คุณควรเห็นเอาต์พุต "Hello, TF World!" ในเทอร์มินัล

คุณสามารถตรวจสอบว่าคําสั่งทำงานเสร็จแล้วโดยใช้ list invocations หรือ l i ในพรอมต์คอนโซล และไม่ควรมีสิ่งใดแสดง หากมีคำสั่งที่ทำงานอยู่ ระบบจะแสดงคำสั่งดังกล่าวดังนี้

tf >l i
Command Id  Exec Time  Device       State
10          0m:00      [876X00GNG]  running stub on build(s) 'BuildInfo{bid=0, target=stub, serial=876X00GNG}'

เพิ่มการกําหนดค่าลงใน classpath (D, I, R)

นอกจากนี้ คุณยังรวมการกําหนดค่าไว้ในไฟล์ JAR ของ Tradefed โดยตรงเพื่อความสะดวกในการทําให้ใช้งานได้ Tradefed จะจดจําการกําหนดค่าทั้งหมดที่อยู่ในโฟลเดอร์ config ใน classpath โดยอัตโนมัติ

ตัวอย่างเช่น ให้ย้ายไฟล์ helloworld.xml ไปยังไลบรารีหลักของ TradeFed (<tree>/tools/tradefederation/core/res/config/example/helloworld.xml) เพื่อสร้าง TradeFed อีกครั้ง รีสตาร์ทคอนโซล TradeFed แล้วขอให้ TradeFed แสดงรายการการกําหนดค่าจาก classpath

tf> list configs
[…]
example/helloworld: Runs the hello world test

ตอนนี้คุณเรียกใช้การกําหนดค่า helloworld ได้โดยใช้

tf> run example/helloworld
05-12 13:21:21 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World!

โต้ตอบกับอุปกรณ์ (D, R)

จนถึงตอนนี้ HelloWorldTest ของเราไม่ได้ทำอะไรที่น่าสนใจ ความเชี่ยวชาญของ Tradefed คือการทดสอบโดยใช้อุปกรณ์ Android ดังนั้นมาเพิ่มอุปกรณ์ Android ในการทดสอบกัน

การทดสอบสามารถรับการอ้างอิงไปยังอุปกรณ์ Android โดยใช้ TestInformation ซึ่งเฟรมเวิร์กจะระบุให้เมื่อมีการเรียกใช้เมธอด IRemoteTest#run

มาแก้ไขข้อความพิมพ์ HelloWorldTest ให้แสดงหมายเลขซีเรียลของอุปกรณ์กัน

@Override
public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber());
}

ตอนนี้ให้สร้าง tradefed อีกครั้งและตรวจสอบรายการอุปกรณ์

tradefed.sh
tf> list devices
Serial            State      Product   Variant   Build   Battery
004ad9880810a548  Available  mako      mako      JDQ39   100

จดหมายเลขซีเรียลที่ระบุว่าพร้อมใช้งาน ซึ่งเป็นอุปกรณ์ที่ควรจัดสรรให้กับ HelloWorld

tf> run example/helloworld
05-12 13:26:18 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World! I have device 004ad9880810a548

คุณควรเห็นข้อความพิมพ์ใหม่แสดงหมายเลขซีเรียลของอุปกรณ์

ส่งผลการทดสอบ (D)

IRemoteTest จะรายงานผลลัพธ์โดยการเรียกใช้เมธอดในอินสแตนซ์ ITestInvocationListener ที่ระบุให้กับเมธอด #run เฟรมเวิร์ก TF จะเป็นผู้รับผิดชอบในการรายงานการเริ่มต้น (ผ่าน ITestInvocationListener#invocationStarted) และการสิ้นสุด (ผ่าน ITestInvocationListener#invocationEnded) ของคําเรียกแต่ละรายการ

การทดสอบการเรียกใช้คือคอลเล็กชันการทดสอบตามตรรกะ หากต้องการรายงานผลลัพธ์การทดสอบ IRemoteTest จะมีหน้าที่รับผิดชอบในการรายงานการเริ่มต้นการทดสอบ การเริ่มต้นและสิ้นสุดการทดสอบแต่ละครั้ง และการสิ้นสุดการทดสอบ

การติดตั้งใช้งาน HelloWorldTest อาจมีลักษณะดังนี้เมื่อผลการทดสอบไม่สําเร็จเพียงรายการเดียว

@Override
public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber());

    TestDescription testId = new TestDescription("com.example.TestClassName", "sampleTest");
    listener.testRunStarted("helloworldrun", 1);
    listener.testStarted(testId);
    listener.testFailed(testId, "oh noes, test failed");
    listener.testEnded(testId, Collections.emptyMap());
    listener.testRunEnded(0, Collections.emptyMap());
}

TF มีIRemoteTestการใช้งานหลายรายการที่คุณนํามาใช้ซ้ำได้แทนที่จะเขียนเองตั้งแต่ต้น ตัวอย่างเช่น InstrumentationTest สามารถเรียกใช้การทดสอบของแอปพลิเคชัน Android จากระยะไกลในอุปกรณ์ Android, แยกวิเคราะห์ผลลัพธ์ และส่งต่อผลลัพธ์เหล่านั้นไปยัง ITestInvocationListener) ดูรายละเอียดได้ที่ประเภทการทดสอบ

ผลการทดสอบ Store (I)

การใช้งานโปรแกรมรับฟังการทดสอบเริ่มต้นสําหรับการกําหนดค่า TF คือ TextResultReporter ซึ่งจะแสดงผลลัพธ์ของการเรียกใช้ไปยัง stdout ตัวอย่างเช่น ให้เรียกใช้การกําหนดค่า HelloWorldTest จากส่วนก่อนหน้า

./tradefed.sh
tf> run example/helloworld
04-29 18:25:55 I/TestInvocation: Invocation was started with cmd: /tmp/helloworld.xml
04-29 18:25:55 I/TestInvocation: Starting invocation for 'stub' with '[ BuildInfo{bid=0, target=stub, serial=876X00GNG} on device '876X00GNG']
04-29 18:25:55 I/HelloWorldTest: Hello, TF World! I have device 876X00GNG
04-29 18:25:55 I/InvocationToJUnitResultForwarder: Running helloworldrun: 1 tests
04-29 18:25:55 W/InvocationToJUnitResultForwarder:
Test com.example.TestClassName#sampleTest failed with stack:
 oh noes, test failed
04-29 18:25:55 I/InvocationToJUnitResultForwarder: Run ended in 0 ms

หากต้องการจัดเก็บผลลัพธ์ของการเรียกใช้ที่อื่น เช่น ในไฟล์ ให้ระบุการใช้งาน ITestInvocationListener ที่กำหนดเองโดยใช้แท็ก result_reporter ในการกำหนดค่า

TF ยังมีตัวรับฟัง XmlResultReporter ซึ่งจะเขียนผลลัพธ์การทดสอบลงในไฟล์ XML ในรูปแบบที่คล้ายกับที่ant JUnit XML Writer ใช้ หากต้องการระบุ result_reporter ในการกำหนดค่า ให้แก้ไข …/res/config/example/helloworld.xml config ดังนี้

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
    <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
</configuration>

ตอนนี้ให้สร้าง tradefed อีกครั้งและเรียกใช้ตัวอย่าง Hello World อีกครั้ง

tf> run example/helloworld
05-16 21:07:07 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World! I have device 004ad9880810a548
05-16 21:07:07 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_2991649128735283633/device_logcat_6999997036887173857.txt
05-16 21:07:07 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_2991649128735283633/host_log_6307746032218561704.txt
05-16 21:07:07 I/XmlResultReporter: XML test result file generated at /tmp/0/inv_2991649128735283633/test_result_536358148261684076.xml. Total tests 1, Failed 1, Error 0

โปรดสังเกตข้อความบันทึกที่ระบุว่ามีการสร้างไฟล์ XML แล้ว ไฟล์ที่สร้างขึ้นควรมีลักษณะดังนี้

<?xml version='1.0' encoding='UTF-8' ?>
<testsuite name="stub" tests="1" failures="1" errors="0" time="9" timestamp="2011-05-17T04:07:07" hostname="localhost">
  <properties />
  <testcase name="sampleTest" classname="com.example.TestClassName" time="0">
    <failure>oh noes, test failed
    </failure>
  </testcase>
</testsuite>

นอกจากนี้ คุณยังเขียนตัวฟังการเรียกใช้ที่กําหนดเองของคุณเองได้ เพียงแค่ต้องใช้งานอินเทอร์เฟซ ITestInvocationListener

Tradefed รองรับตัวรับฟังการเรียกใช้หลายรายการ คุณจึงส่งผลการทดสอบไปยังปลายทางอิสระหลายแห่งได้ โดยในการดำเนินการ เพียงแค่ระบุแท็ก <result_reporter> หลายรายการในการกําหนดค่า

สถานที่บันทึก (D, I, R)

เครื่องมือบันทึกของ TF มีความสามารถในการดำเนินการต่อไปนี้

  1. บันทึกจากอุปกรณ์ (หรือที่เรียกว่า Logcat ของอุปกรณ์)
  2. บันทึกบันทึกจากเฟรมเวิร์ก Trade Federation ที่ทำงานบนเครื่องโฮสต์ (หรือที่เรียกว่าบันทึกของโฮสต์)

เฟรมเวิร์ก TF จะบันทึก Logcat จากอุปกรณ์ที่จัดสรรโดยอัตโนมัติ และส่งไปยัง Invocation Listener เพื่อประมวลผล XmlResultReporter จะบันทึกบันทึก Logcat ของอุปกรณ์ที่บันทึกไว้เป็นไฟล์

ระบบจะรายงานบันทึกของโฮสต์ TF โดยใช้ CLog wrapper สำหรับคลาสบันทึก ddmlib มาแปลงการเรียก System.out.println ก่อนหน้านี้ใน HelloWorldTest เป็นการเรียก CLog กัน

@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device %s", getDevice().getSerialNumber());

CLog จัดการการแทรกสตริงโดยตรง ซึ่งคล้ายกับ String.format เมื่อสร้างใหม่และเรียกใช้ TF อีกครั้ง คุณควรเห็นข้อความบันทึกใน stdout ดังนี้

tf> run example/helloworld
…
05-16 21:30:46 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548
…

โดยค่าเริ่มต้น tradefed จะแสดงผลข้อความบันทึกของโฮสต์ไปยัง stdout TF ยังมีการติดตั้งใช้งานบันทึกที่เขียนข้อความลงในไฟล์ด้วย นั่นคือ FileLogger หากต้องการเพิ่มการบันทึกไฟล์ ให้เพิ่มแท็ก logger ลงในการกําหนดค่า โดยระบุชื่อคลาสแบบเต็มของ FileLogger

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
    <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
    <logger class="com.android.tradefed.log.FileLogger" />
</configuration>

ตอนนี้ให้สร้างและเรียกใช้ตัวอย่าง helloworld อีกครั้ง

tf >run example/helloworld
…
05-16 21:38:21 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_6390011618174565918/device_logcat_1302097394309452308.txt
05-16 21:38:21 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt
…

ข้อความบันทึกจะระบุเส้นทางของบันทึกของโฮสต์ ซึ่งเมื่อดูแล้วควรมีข้อความบันทึก HelloWorldTest ดังนี้

more /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt

ตัวอย่างเอาต์พุต

…
05-16 21:38:21 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548

ตัวเลือกการจัดการ (D, I, R)

ออบเจ็กต์ที่โหลดจากการกําหนดค่า TF (หรือที่เรียกว่าออบเจ็กต์การกําหนดค่า) ยังรับข้อมูลจากอาร์กิวเมนต์บรรทัดคําสั่งได้โดยใช้การกำกับเนื้อหา @Option

หากต้องการเข้าร่วม คลาสออบเจ็กต์การกําหนดค่าจะใช้@Option คําอธิบายประกอบกับช่องสมาชิกและตั้งชื่อที่ไม่ซ้ำกัน ซึ่งจะช่วยให้ระบบป้อนข้อมูลค่าช่องสมาชิกผ่านตัวเลือกบรรทัดคำสั่งได้ (และยังเพิ่มตัวเลือกนั้นลงในระบบความช่วยเหลือในการกําหนดค่าโดยอัตโนมัติด้วย)

หมายเหตุ: ระบบอาจไม่รองรับประเภทฟิลด์บางประเภท ดูคำอธิบายประเภทที่รองรับได้ที่ OptionSetter

มาเพิ่ม @Option ลงใน HelloWorldTest กัน

@Option(name="my_option",
        shortName='m',
        description="this is the option's help text",
        // always display this option in the default help text
        importance=Importance.ALWAYS)
private String mMyOption = "thisisthedefault";

ต่อไปมาเพิ่มข้อความบันทึกเพื่อแสดงค่าของตัวเลือกใน HelloWorldTest เพื่อให้เราสาธิตว่าได้รับค่าอย่างถูกต้อง

@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    …
    CLog.logAndDisplay(LogLevel.INFO, "I received option '%s'", mMyOption);

สุดท้าย ให้สร้าง TF อีกครั้งและเรียกใช้ helloworld คุณควรเห็นข้อความบันทึกที่มีค่าเริ่มต้นmy_option ดังนี้

tf> run example/helloworld
…
05-24 18:30:05 I/HelloWorldTest: I received option 'thisisthedefault'

ส่งค่าจากบรรทัดคำสั่ง

ส่งค่าสำหรับ my_option คุณควรเห็น my_option สร้างขึ้นด้วยค่านั้น

tf> run example/helloworld --my_option foo
…
05-24 18:33:44 I/HelloWorldTest: I received option 'foo'

การกําหนดค่า TF ยังมีระบบความช่วยเหลือด้วย ซึ่งจะแสดงข้อความช่วยเหลือสําหรับช่อง @Option โดยอัตโนมัติ ลองเลย คุณควรเห็นข้อความช่วยเหลือสำหรับ my_option ดังนี้

tf> run example/helloworld --help
Printing help for only the important options. To see help for all options, use the --help-all flag

  cmd_options options:
    --[no-]help          display the help text for the most important/critical options. Default: false.
    --[no-]help-all      display the full help text for all options. Default: false.
    --[no-]loop          keep running continuously. Default: false.

  test options:
    -m, --my_option      this is the option's help text Default: thisisthedefault.

  'file' logger options:
    --log-level-display  the minimum log level to display on stdout. Must be one of verbose, debug, info, warn, error, assert. Default: error.

โปรดสังเกตข้อความ "การพิมพ์เฉพาะตัวเลือกที่สําคัญ" TF ใช้แอตทริบิวต์ Option#importance เพื่อพิจารณาว่าจะแสดงข้อความความช่วยเหลือของช่อง @Option รายการใดเมื่อมีการระบุ --help หรือไม่ เพื่อลดความกระจัดกระจายของความช่วยเหลือเกี่ยวกับตัวเลือก --help-all จะแสดงความช่วยเหลือสำหรับฟิลด์ @Option ทั้งหมดเสมอ ไม่ว่าจะสำคัญหรือไม่สำคัญ โปรดดูรายละเอียดที่ Option.Importance

ส่งค่าจากการกําหนดค่า

นอกจากนี้ คุณยังระบุค่าตัวเลือกภายในการกําหนดค่าได้โดยการเพิ่มองค์ประกอบ <option name="" value=""> ทดสอบโดยใช้helloworld.xml

<test class="com.android.tradefed.example.HelloWorldTest" >
    <option name="my_option" value="fromxml" />
</test>

การสร้างและเรียกใช้ helloworld อีกครั้งควรให้เอาต์พุตนี้

05-24 20:38:25 I/HelloWorldTest: I received option 'fromxml'

ความช่วยเหลือในการกําหนดค่าควรอัปเดตเพื่อระบุค่าเริ่มต้นของ my_option ดังนี้

tf> run example/helloworld --help
  test options:
    -m, --my_option      this is the option's help text Default: fromxml.

ออบเจ็กต์การกําหนดค่าอื่นๆ ที่รวมอยู่ในการกําหนดค่า helloworld เช่น FileLogger ก็ยอมรับตัวเลือกได้เช่นกัน ตัวเลือก --log-level-display น่าสนใจเนื่องจากจะกรองบันทึกที่แสดงใน stdout ก่อนหน้านี้ในบทแนะนํา คุณอาจเห็นข้อความ "สวัสดี TF World ข้อความบันทึกของอุปกรณ์ … หยุดแสดงใน stdout หลังจากที่เราเปลี่ยนไปใช้ FileLogger คุณสามารถเพิ่มรายละเอียดของการบันทึกไปยัง stdout ได้โดยส่งอาร์กิวเมนต์ --log-level-display

ลองทำตอนนี้ คุณควรเห็นข้อความบันทึก "ฉันมีอุปกรณ์" ปรากฏขึ้นอีกครั้งใน stdout นอกเหนือจากการบันทึกลงในไฟล์

tf> run example/helloworld --log-level-display info
…
05-24 18:53:50 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548

เท่านี้ก่อนนะ

โปรดทราบว่าหากพบปัญหา ซอร์สโค้ดของ Trade Federation มีข้อมูลที่เป็นประโยชน์มากมายที่ไม่ได้แสดงในเอกสารประกอบ หากวิธีอื่นไม่ได้ผล ให้ลองถามใน Google Group ของ android-platform โดยใส่ "Trade Federation" ในหัวเรื่องข้อความ