• 传到网盘里分享一下

  • 方便上传下那个 log_20181025_101740 文件夹吗?看你截图分析不出来啥问题

  • 大牛不敢当,对你有帮助就好

  • 看信息应该是图片没找到,你看看生成日志文件夹里有对应的图片吗?正常情况文件夹里应该包含图片和 log.txt

  • runner.py 改成下面的代码就不用封装方法了

    # -*- coding: utf-8 -*-
    
    import unittest
    import os
    import sys
    import six
    import traceback
    import types
    import time
    from io import open
    from airtest.cli.parser import runner_parser
    from airtest.core.api import G, auto_setup, log
    from airtest.core.settings import Settings as ST
    from airtest.utils.compat import decode_path
    from copy import copy
    
    class MyAirtestCase(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            cls.args = args
    
            setup_by_args(args)
    
            # setup script exec scope
            cls.scope = copy(globals())
    
        def setUp(self):
            if self.args.log and self.args.recording:
                for dev in G.DEVICE_LIST:
                    try:
                        dev.start_recording()
                    except:
                        traceback.print_exc()
            # 设置日志路径
            auto_setup(logdir=self._logdir)
    
        def tearDown(self):
            if self.args.log and self.args.recording:
                for k, dev in enumerate(G.DEVICE_LIST):
                    try:
                        output = os.path.join(self.args.log, "recording_%d.mp4" % k)
                        dev.stop_recording(output)
                    except:
                        traceback.print_exc()
    
        def runTest(self):
            try:
                # 调用脚本中的runCase方法并传递scope供脚本使用
                exec(self._code["code"], self._code["ns"])
            except Exception as err:
                tb = traceback.format_exc()
                log("Final Error", tb)
                six.reraise(*sys.exc_info())
    
        @property
        def logdir(self):
            return self._logdir
    
        @logdir.setter
        def logdir(self, value):
            self._logdir = value
    
        @property
        def code(self):
            return self._code
    
        @code.setter
        def code(self, value):
            self._code = value
    
    def setup_by_args(args):
        # init devices
        if isinstance(args.device, list):
            devices = args.device
        elif args.device:
            devices = [args.device]
        else:
            devices = []
            print("do not connect device")
    
        # set base dir to find tpl
        args.script = decode_path(args.script)
    
        # set log dir
        if args.log is True:
            print("save log in %s/log" % args.script)
            args.log = os.path.join(args.script, "log")
        elif args.log:
            print("save log in '%s'" % args.log)
            args.log = decode_path(args.log)
        else:
            print("do not save log")
    
        # guess project_root to be basedir of current .air path
        project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
        # 此处不设置日志路径,防止生成多余的log.txt
        auto_setup(args.script, devices, None, project_root)
    
    def new_case(py, logdir):
        """实例化MyAirtestCase并绑定runCase方法"""
        with open(py, 'r', encoding="utf8") as f:
            code = f.read()
        obj = compile(code.encode("utf-8"), py, "exec")
        ns = {}
        ns["__file__"] = py
    
        case = MyAirtestCase()
        pyfilename = os.path.basename(py).replace(".py", "")
        # 设置属性以便在setUp中设置日志路径
        case.logdir = os.path.join(logdir, pyfilename)
        case.code = {"code": obj, "ns": ns}
        return case
    
    def init_log_folder():
        """初始化日志根目录"""
        name = time.strftime("log_%Y%m%d_%H%M%S", time.localtime())
        if not os.path.exists(name):
            os.mkdir(name)
        return name
    
    def run_script(parsed_args, testcase_cls=MyAirtestCase):
        global args  # make it global deliberately to be used in MyAirtestCase & test scripts
        args = parsed_args
        dir = os.path.dirname(os.path.realpath(__file__))
        suites = []
        pys = []
    
        # 获取所有用例集
        for f in os.listdir(dir):
            if f.endswith("air"):
                f = os.path.join(dir, f)
                if os.path.isdir(f):
                    suites.append(f)
    
        # 获取所有脚本
        for s in suites:
            for f in os.listdir(s):
                if f.endswith(".py") and not f.startswith("__"):
                    pys.append(os.path.join(s, f))
    
        logdir = os.path.join(dir, init_log_folder())
        args.log = logdir
        suite = unittest.TestSuite()
    
        # 添加脚本
        for py in pys:
            case = new_case(py, logdir)
            suite.addTest(case)
    
        result = unittest.TextTestRunner(verbosity=0).run(suite)
        if not result.wasSuccessful():
            sys.exit(-1)
    
    if __name__ == "__main__":
        ap = runner_parser()
        args = ap.parse_args()
        run_script(args, MyAirtestCase)
    
  • 报错是因为需要将测试代码封装成一个 runCase 方法,像下面那样

    def runCase(self, vars):
        # 业务逻辑代码
        pass
    

    代码在这里 new_case 方法

    之所以这么整是考虑后面要实现数据驱动,可以通过方法签名里那个 vars 参数来传递数据,这样看起来比直接在命名空间里获取数据稍微清晰一点

  • 代码里加了过滤,文件夹必须以 “用例集” 结尾
    代码链接

    目录结构如下就能识别出来

    case
    ├─report.py
    ├─runner.py
    ├─交易用例集
    │      ├──交易失败.py
    │      └──交易成功.py
    └─登录用例集
            ├──登录失败.py
            └──登录成功.py
    
  • 参数校验两种方式:

    • 使用@Valid注解自动校验
    public Mono<Map<String, Object>> registerService(@Valid User user) {
        return userRepository.save(user).map(u -> {
            Map<String, Object> map = new HashMap<>(2);
            map.put("status", 1);
            return map;
        });
    }
    
    // User
    public class User {
        @NotNull
        private String username;
        @NotNull
        private String password;
        // getter and setter
    }
    
    • 手动校验
    public Mono<Map<String, Object>> registerService(User user) {
        Map<String, Object> map = new HashMap<>(2);
        return Mono.just(user).flatMap(u1 -> {
            if (u1.getUsername() == null) {
                map.put("status", 0);
                map.put("reason", "username is null");
                return Mono.just(map);
            }
            if (u1.getPassword() == null) {
                map.put("status", 0);
                map.put("reason", "password is null");
                return Mono.just(map);
            }
            return userRepository.save(u1).map(u2 -> {
                map.put("status", 1);
                return map;
            });
        });
    }
    
  • 如果没记错的话,貌似是这么写

    public Mono<Map<String, Object>> registerService(User user) {
        return userRepository.save(user).map(u -> {
            Map<String, Object> map = new HashMap<>(2);
            map.put("status", 1);
            return map;
        });
    }
    
  • 求两个有序数组的中位数 at 2018年08月20日

    二路归并改一下,时间复杂度 O(n),空间复杂度 O(1)

    public static int middle(int[] a, int[] b) {
        int middleIndex = (a.length + b.length + 1) / 2 - 1;
        if (middleIndex == 0) {
            return -1;
        }
        int tmp;
        int i = 0, j = 0, k = 0;
        for (; i < a.length && j < b.length; ++k) {
            if (a[i] < b[j]) {
                tmp = a[i++];
            } else {
                tmp = b[j++];
            }
            if (k == middleIndex) {
                return tmp;
            }
        }
        if (i < a.length) {
            return a[middleIndex - k + i];
        }
        return b[middleIndex - k + j];
    }
    
  • org.testng.IMethodInterceptor

    API 中说道:This class is used to alter the list of test methods that TestNG is about to run.

    API 链接

  • 测试人员的价值何在? at 2017年09月04日

    背锅😂

  • driver.executeScript("arguments[0].innerText = 'your content'", element);
    
  • #1 楼 @michael_wang 那个元素确认已经找到了,下面是 server 的响应,貌似是 server 没有实现对应的方法 😟 😟 😟

    unknown command: session/58ebe28bd7ec856ff236702d0abbc84e/element/0.38716481951996684-1/rect
    
  • 可以将元素定位信息外提形成对象库,Android 和 iOS 分别使用各自的对象库,业务逻辑写一套就行

  • @debugtalk 楼主我有两个问题:
    1、需要判断结果的情况该如何处理,比如一步操作的结果可能会返回 3 种结果:1、2、3,当 1 的时候需要执行操作 A,2 的时候执行操作 B,3 的时候执行操作 C
    2、执行时需要前一步的结果,比如输入验证码的过程,需要先获取文本框中的验证码,然后再在编辑框中输入

    不知道楼主框架中对这两种情况是如何处理的,本人才疏学浅还望楼主赐教

  • #2 楼 @lucifer 别的方法估计只能用 findElements 获取到一个 List,然后再在 List 中去取了
    例如

    List<WebElement> ls = driver.findElements(By.className("your classname"));
    WebElement e = ls.get(10);
    
  • appium 定位 hybird 的方法 at 2016年08月30日

    用 chrome 浏览器 chrome://inspect 可以远程调试 Android 的 webview,html 里面应该是有 id 等属性的吧

  • 你看看这样行不行
    xpath:(//LinearLayout)[7]/TextView

  • 另外如果 Android SDK 的路径也带空格的话,运行上面代码也会报错,提示 Internal Server Error
    @xdf

  • @fengliuyishao 直接用 findElementByIosUIAutomation 方法就行,若 element 不可见,则自动滑动到 element 位置。

    官方 IOSElement#scrollTo 的 API 有云:
    This method is deprecated because it is not consistent and it is going to be removed. It is workaround actually. Recommended to use instead: AppiumDriver.swipe(int, int, int, int, int) MobileElement.swipe(SwipeElementDirection, int) MobileElement.swipe(SwipeElementDirection, int, int, int) or search for elements using MobileBy.ByIosUIAutomation