qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          使用JNA解決自動化測試無法做密碼輸入操作的問題

           在做頁面自動化(以使用selenium為例)的時候,很常見的一個場景就是輸入密碼。往往對于輸入框都使用WebElement的sendKeys(CharSequence... keysToSend)的方法。

            Java代碼

          1./**
          2.   * Use this method to simulate typing into an element, which may set its value.
          3.   */ 
          4.  void sendKeys(CharSequence... keysToSend);

            一般情況下這個方法是可以勝任的,但是現在很多網站為了安全性的考慮都會對密碼輸入框做特殊的處理,而且不同的瀏覽器也不同。例如支付寶。

            支付寶輸入密碼控件在Chrome瀏覽器下

            支付寶輸入密碼控件在Firefox瀏覽器下

            支付寶輸入密碼控件在IE(IE8)瀏覽器下

            可見在不同的瀏覽器下是有差異的。那么現在存在兩個問題。首先,selenium的sendKeys方法無法操作這樣特殊的控件;其次,不同瀏覽器又存在差異,搞定了chrome,在IE下又不能用,這樣又要解決瀏覽器兼容性問題。

          如何解決這兩個問題呢?

            我們可以發現平時人工使用鍵盤輸入密碼的時候是沒有這些問題的,那么我們是否可以模擬人工操作時的鍵盤輸入方式呢?答案是肯定的,使用操作系統的API,模擬鍵盤發送消息事件給操作系統,可以避免所有瀏覽器等差異和安全性帶來的問題。

            我個人建議使用JNA(https://github.com/twall/jna),JNA是一種和JNI類似的技術,但是相對JNI來說更加易用。 JNA共有jna.jar和platform.jar兩個依賴庫,都需要引入,我們需要用到的在platform.jar中。從包結構可以看出,JNA中包含了mac、unix、win32等各類操作系統的系統API映射。如下圖:

            系統API映射關系在JNA的文章中有描述,如下:

            數據類型的映射參見:https://github.com/twall/jna/blob/master/www/Mappings.md

            本文中以windows為例演示下如何在支付寶的密碼安全控件中輸入密碼。

            JNA中關于windows平臺的是com.sun.jna.platform.win32包中User32這個接口。這里映射了很多windows系統API可以使用。但是我們需要用到的SendMessage卻沒有。所以需要新建一個接口,映射SendMessage函數。代碼如下:

          1.import com.sun.jna.Native;  
          2.import com.sun.jna.platform.win32.User32;  
          3.import com.sun.jna.win32.W32APIOptions;  
          4.  
          5.public interface User32Ext extends User32 {  
          6.  
          7.    User32Ext USER32EXT = (User32Ext) Native.loadLibrary("user32", User32Ext.class, W32APIOptions.DEFAULT_OPTIONS);  
          8.      
          9.    /** 
          10.     * 查找窗口 
          11.     * @param lpParent 需要查找窗口的父窗口 
          12.     * @param lpChild 需要查找窗口的子窗口 
          13.     * @param lpClassName 類名 
          14.     * @param lpWindowName 窗口名 
          15.     * @return 找到的窗口的句柄 
          16.     */  
          17.    HWND FindWindowEx(HWND lpParent, HWND lpChild, String lpClassName, String lpWindowName);  
          18.  
          19.    /** 
          20.     * 獲取桌面窗口,可以理解為所有窗口的root 
          21.     * @return 獲取的窗口的句柄 
          22.     */  
          23.    HWND GetDesktopWindow();  
          24.      
          25.    /** 
          26.     * 發送事件消息 
          27.     * @param hWnd 控件的句柄 
          28.     * @param dwFlags 事件類型 
          29.     * @param bVk 虛擬按鍵碼 
          30.     * @param dwExtraInfo 擴展信息,傳0即可 
          31.     * @return 
          32.     */  
          33.    int SendMessage(HWND hWnd, int dwFlags, byte bVk, int dwExtraInfo);  
          34.  
          35.    /** 
          36.     * 發送事件消息 
          37.     * @param hWnd 控件的句柄 
          38.     * @param Msg 事件類型 
          39.     * @param wParam 傳0即可 
          40.     * @param lParam 需要發送的消息,如果是點擊操作傳null 
          41.     * @return 
          42.     */  
          43.    int SendMessage(HWND hWnd, int Msg, int wParam, String lParam);  
          44.      
          45.    /** 
          46.     * 發送鍵盤事件 
          47.     * @param bVk 虛擬按鍵碼 
          48.     * @param bScan 傳 ((byte)0) 即可 
          49.     * @param dwFlags 鍵盤事件類型 
          50.     * @param dwExtraInfo 傳0即可 
          51.     */  
          52.    void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);  
          53.      
          54.    /** 
          55.     * 激活指定窗口(將鼠標焦點定位于指定窗口) 
          56.     * @param hWnd 需激活的窗口的句柄 
          57.     * @param fAltTab 是否將最小化窗口還原 
          58.     */  
          59.    void SwitchToThisWindow(HWND hWnd, boolean fAltTab);  
          60.      
          61.}

           系統API映射好以后,利用這個接口寫了如下的工具類,包含點擊和輸入各種操作。代碼如下:

          1.import java.util.concurrent.Callable;  
          2.import java.util.concurrent.ExecutorService;  
          3.import java.util.concurrent.Executors;  
          4.import java.util.concurrent.Future;  
          5.import java.util.concurrent.TimeUnit;  
          6.  
          7.import com.sun.jna.Native;  
          8.import com.sun.jna.Pointer;  
          9.import com.sun.jna.platform.win32.WinDef.HWND;  
          10.import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;  
          11.  
          12./** 
          13. * Window組件操作工具類 
          14. *  
          15. * @author sunju 
          16. *  
          17. */  
          18.public class Win32Util {  
          19.  
          20.    private static final int N_MAX_COUNT = 512;  
          21.  
          22.    private Win32Util() {  
          23.    }  
          24.  
          25.    /** 
          26.     * 從桌面開始查找指定類名的組件,在超時的時間范圍內,如果未找到任何匹配的組件則反復查找 
          27.     * @param className 組件的類名 
          28.     * @param timeout 超時時間 
          29.     * @param unit 超時時間的單位 
          30.     * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到或超時則返回<code>null</code> 
          31.     */  
          32.    public static HWND findHandleByClassName(String className, long timeout, TimeUnit unit) {  
          33.        return findHandleByClassName(USER32EXT.GetDesktopWindow(), className, timeout, unit);  
          34.    }  
          35.  
          36.    /** 
          37.     * 從桌面開始查找指定類名的組件 
          38.     * @param className 組件的類名 
          39.     * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到任何匹配則返回<code>null</code> 
          40.     */  
          41.    public static HWND findHandleByClassName(String className) {  
          42.        return findHandleByClassName(USER32EXT.GetDesktopWindow(), className);  
          43.    }  
          44.  
          45.    /** 
          46.     * 從指定位置開始查找指定類名的組件 
          47.     * @param root 查找組件的起始位置的組件的句柄,如果為<code>null</code>則從桌面開始查找 
          48.     * @param className 組件的類名 
          49.     * @param timeout 超時時間 
          50.     * @param unit 超時時間的單位 
          51.     * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到或超時則返回<code>null</code> 
          52.     */  
          53.    public static HWND findHandleByClassName(HWND root, String className, long timeout, TimeUnit unit) {  
          54.        if(null == className || className.length() <= 0) {  
          55.            return null;  
          56.        }  
          57.        long start = System.currentTimeMillis();  
          58.        HWND hwnd = findHandleByClassName(root, className);  
          59.        while(null == hwnd && (System.currentTimeMillis() - start < unit.toMillis(timeout))) {  
          60.            hwnd = findHandleByClassName(root, className);  
          61.        }  
          62.        return hwnd;  
          63.    }  
          64.  
          65.    /** 
          66.     * 從指定位置開始查找指定類名的組件 
          67.     * @param root 查找組件的起始位置的組件的句柄,如果為<code>null</code>則從桌面開始查找 
          68.     * @param className 組件的類名 
          69.     * @return 返回匹配的組件的句柄,如果匹配的組件大于一個,返回第一個查找的到的;如果未找到任何匹配則返回<code>null</code> 
          70.     */  
          71.    public static HWND findHandleByClassName(HWND root, String className) {  
          72.        if(null == className || className.length() <= 0) {  
          73.            return null;  
          74.        }  
          75.        HWND[] result = new HWND[1];  
          76.        findHandle(result, root, className);  
          77.        return result[0];  
          78.    }  
          79.  
          80.    private static boolean findHandle(final HWND[] target, HWND root, final String className) {  
          81.        if(null == root) {  
          82.            root = USER32EXT.GetDesktopWindow();  
          83.        }  
          84.        return USER32EXT.EnumChildWindows(root, new WNDENUMPROC() {  
          85.  
          86.            @Override  
          87.            public boolean callback(HWND hwnd, Pointer pointer) {  
          88.                char[] winClass = new char[N_MAX_COUNT];  
          89.                USER32EXT.GetClassName(hwnd, winClass, N_MAX_COUNT);  
          90.                if(USER32EXT.IsWindowVisible(hwnd) && className.equals(Native.toString(winClass))) {  
          91.                    target[0] = hwnd;  
          92.                    return false;  
          93.                } else {  
          94.                    return target[0] == null || findHandle(target, hwnd, className);  
          95.                }  
          96.            }  
          97.  
          98.        }, Pointer.NULL);  
          99.    }  
          100.  
          101.    /** 
          102.     * 模擬鍵盤按鍵事件,異步事件。使用win32 keybd_event,每次發送KEYEVENTF_KEYDOWN、KEYEVENTF_KEYUP兩個事件。默認10秒超時 
          103.     * @param hwnd 被鍵盤操作的組件句柄 
          104.     * @param keyCombination 鍵盤的虛擬按鍵碼(<a href="http://msdn.microsoft.com/ZH-CN/library/windows/desktop/dd375731.aspx">Virtual-Key Code</a>),或者使用{@link java.awt.event.KeyEvent}</br> 
          105.     *                      二維數組第一維中的一個元素為一次按鍵操作,包含組合操作,第二維中的一個元素為一個按鍵事件,即一個虛擬按鍵碼 
          106.     * @return 鍵盤按鍵事件放入windows消息隊列成功返回<code>true</code>,鍵盤按鍵事件放入windows消息隊列失敗或超時返回<code>false</code> 
          107.     */  
          108.    public static boolean simulateKeyboardEvent(HWND hwnd, int[][] keyCombination) {  
          109.        if(null == hwnd) {  
          110.            return false;  
          111.        }  
          112.        USER32EXT.SwitchToThisWindow(hwnd, true);  
          113.        USER32EXT.SetFocus(hwnd);  
          114.        for(int[] keys : keyCombination) {  
          115.            for(int i = 0; i < keys.length; i++) {  
          116.                USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYDOWN, 0); // key down  
          117.            }  
          118.            for(int i = keys.length - 1; i >= 0; i--) {  
          119.                USER32EXT.keybd_event((byte) keys[i], (byte) 0, KEYEVENTF_KEYUP, 0); // key up  
          120.            }  
          121.        }  
          122.        return true;  
          123.    }  
          124.  
          125.    /** 
          126.     * 模擬字符輸入,同步事件。使用win32 SendMessage API發送WM_CHAR事件。默認10秒超時 
          127.     * @param hwnd 被輸入字符的組件的句柄 
          128.     * @param content 輸入的內容。字符串會被轉換成<code>char[]</code>后逐個字符輸入 
          129.     * @return 字符輸入事件發送成功返回<code>true</code>,字符輸入事件發送失敗或超時返回<code>false</code> 
          130.     */  
          131.    public static boolean simulateCharInput(final HWND hwnd, final String content) {  
          132.        if(null == hwnd) {  
          133.            return false;  
          134.        }  
          135.        try {  
          136.            return execute(new Callable<Boolean>() {  
          137.  
          138.                @Override  
          139.                public Boolean call() throws Exception {  
          140.                    USER32EXT.SwitchToThisWindow(hwnd, true);  
          141.                    USER32EXT.SetFocus(hwnd);  
          142.                    for(char c : content.toCharArray()) {  
          143.                        Thread.sleep(5);  
          144.                        USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);  
          145.                    }  
          146.                    return true;  
          147.                }  
          148.  
          149.            });  
          150.        } catch(Exception e) {  
          151.            return false;  
          152.        }  
          153.    }  
          154.      
          155.    public static boolean simulateCharInput(final HWND hwnd, final String content, final long sleepMillisPreCharInput) {  
          156.        if(null == hwnd) {  
          157.            return false;  
          158.        }  
          159.        try {  
          160.            return execute(new Callable<Boolean>() {  
          161.  
          162.                @Override  
          163.                public Boolean call() throws Exception {  
          164.                    USER32EXT.SwitchToThisWindow(hwnd, true);  
          165.                    USER32EXT.SetFocus(hwnd);  
          166.                    for(char c : content.toCharArray()) {  
          167.                        Thread.sleep(sleepMillisPreCharInput);  
          168.                        USER32EXT.SendMessage(hwnd, WM_CHAR, (byte) c, 0);  
          169.                    }  
          170.                    return true;  
          171.                }  
          172.  
          173.            });  
          174.        } catch(Exception e) {  
          175.            return false;  
          176.        }  
          177.    }  
          178.  
          179.    /** 
          180.     * 模擬文本輸入,同步事件。使用win32 SendMessage API發送WM_SETTEXT事件。默認10秒超時 
          181.     * @param hwnd 被輸入文本的組件的句柄 
          182.     * @param content 輸入的文本內容 
          183.     * @return 文本輸入事件發送成功返回<code>true</code>,文本輸入事件發送失敗或超時返回<code>false</code> 
          184.     */  
          185.    public static boolean simulateTextInput(final HWND hwnd, final String content) {  
          186.        if(null == hwnd) {  
          187.            return false;  
          188.        }  
          189.        try {  
          190.            return execute(new Callable<Boolean>() {  
          191.  
          192.                @Override  
          193.                public Boolean call() throws Exception {  
          194.                    USER32EXT.SwitchToThisWindow(hwnd, true);  
          195.                    USER32EXT.SetFocus(hwnd);  
          196.                    USER32EXT.SendMessage(hwnd, WM_SETTEXT, 0, content);  
          197.                    return true;  
          198.                }  
          199.  
          200.            });  
          201.        } catch(Exception e) {  
          202.            return false;  
          203.        }  
          204.    }  
          205.  
          206.    /** 
          207.     * 模擬鼠標點擊,同步事件。使用win32 SendMessage API發送BM_CLICK事件。默認10秒超時 
          208.     * @param hwnd 被點擊的組件的句柄 
          209.     * @return 點擊事件發送成功返回<code>true</code>,點擊事件發送失敗或超時返回<code>false</code> 
          210.     */  
          211.    public static boolean simulateClick(final HWND hwnd) {  
          212.        if(null == hwnd) {  
          213.            return false;  
          214.        }  
          215.        try {  
          216.            return execute(new Callable<Boolean>() {  
          217.  
          218.                @Override  
          219.                public Boolean call() throws Exception {  
          220.                    USER32EXT.SwitchToThisWindow(hwnd, true);  
          221.                    USER32EXT.SendMessage(hwnd, BM_CLICK, 0, null);  
          222.                    return true;  
          223.                }  
          224.  
          225.            });  
          226.        } catch(Exception e) {  
          227.            return false;  
          228.        }  
          229.    }  
          230.  
          231.    private static <T> T execute(Callable<T> callable) throws Exception {  
          232.        ExecutorService executor = Executors.newSingleThreadExecutor();  
          233.        try {  
          234.            Future<T> task = executor.submit(callable);  
          235.            return task.get(10, TimeUnit.SECONDS);  
          236.        } finally {  
          237.            executor.shutdown();  
          238.        }  
          239.    }  
          240.}

          其中用到的各種事件類型定義如下:

          1.public class Win32MessageConstants {  
          2.  
          3.    public static final int WM_SETTEXT = 0x000C; //輸入文本  
          4.      
          5.    public static final int WM_CHAR = 0x0102; //輸入字符  
          6.  
          7.    public static final int BM_CLICK = 0xF5; //點擊事件,即按下和抬起兩個動作  
          8.  
          9.    public static final int KEYEVENTF_KEYUP = 0x0002; //鍵盤按鍵抬起  
          10.      
          11.    public static final int KEYEVENTF_KEYDOWN = 0x0; //鍵盤按鍵按下  
          12.  
          13.}

            下面寫一段測試代碼來測試支付寶密碼安全控件的輸入,測試代碼如下:

          1.import java.util.concurrent.TimeUnit;  
          2.  
          3.import static org.hamcrest.core.Is.is;  
          4.import static org.junit.Assert.assertThat;  
          5.  
          6.import static org.hamcrest.core.IsNull.notNullValue;  
          7.import org.junit.Test;  
          8.  
          9.import com.sun.jna.platform.win32.WinDef;  
          10.import com.sun.jna.platform.win32.WinDef.HWND;  
          11.  
          12.public class AlipayPasswordInputTest {  
          13.  
          14.    @Test  
          15.    public void testAlipayPasswordInput() {  
          16.        String password = "your password";  
          17.        HWND alipayEdit = findHandle("Chrome_RenderWidgetHostHWND", "Edit"); //Chrome瀏覽器,使用Spy++可以抓取句柄的參數  
          18.        assertThat("獲取支付寶密碼控件失敗。", alipayEdit, notNullValue());  
          19.        boolean isSuccess = Win32Util.simulateCharInput(alipayEdit, password);  
          20.        assertThat("輸入支付寶密碼["+ password +"]失敗。", isSuccess,  is(true));  
          21.    }  
          22.      
          23.    private WinDef.HWND findHandle(String browserClassName, String alieditClassName) {  
          24.        WinDef.HWND browser = Win32Util.findHandleByClassName(browserClassName, 10, TimeUnit.SECONDS);  
          25.        return Win32Util.findHandleByClassName(browser, alieditClassName, 10, TimeUnit.SECONDS);  
          26.    }  
          27.}

            測試一下,看看是不是輸入成功了!

            最后說下這個方法的缺陷,任何方法都有不可避免的存在一些問題,完美的事情很少。

            1、sendMessage和postMessage有很多重載的函數,不是每種都有效,從上面的Win32Util中就能看出,實現了很多個方法,需要嘗試下,成本略高;

            2、輸入時需要注意頻率,輸入太快可能導致瀏覽器中安全控件崩潰,支付寶的安全控件在Firefox下輸入太快就會崩潰;

            3、因為是系統API,所以MAC、UNIX、WINDOWS下都不同,如果只是在windows環境下運行,可以忽略;

            4、從測試代碼可以看到,是針對Chrome瀏覽器的,因為每種瀏覽器的窗口句柄不同,所以要區分,不過這個相對簡單,只是名稱不同;

            5、如果你使用Selenium的RemoteDriver,并且是在遠程機器上運行腳本,這個方法會失效。因為remoteDriver最終是http操作,對操作系統API的操作是客戶端行為,不能被翻譯成Http Command,所以會失效。

          posted on 2013-04-08 10:07 順其自然EVO 閱讀(3391) 評論(0)  編輯  收藏 所屬分類: selenium and watir webdrivers 自動化測試學習

          <2013年4月>
          31123456
          78910111213
          14151617181920
          21222324252627
          2829301234
          567891011

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 双柏县| 金乡县| 永州市| 寿宁县| 介休市| 德昌县| 宜春市| 麻江县| 克什克腾旗| 温宿县| 嘉善县| 林州市| 武山县| 高碑店市| 嘉定区| 穆棱市| 民和| 大石桥市| 都昌县| 河曲县| 甘孜| 西畴县| 内黄县| 遂平县| 涡阳县| 绍兴市| 当雄县| 龙岩市| 略阳县| 大埔县| 铁力市| 马山县| 同江市| 弋阳县| 林周县| 科技| 泰兴市| 连州市| 福泉市| 当雄县| 中西区|