Groovy 中的funtion 和 Closure

背景

使用gradle 构建的项目中会使用groovy语言来完成项目构建的配置文件build.gradle 的编写, 为了更好的写这个配置文件, 所以花了点时间深入学习了一下groovy这么语言.groovy可以看做是JVM上的一门脚本语言, 它可以无缝的使用JDK中的jar包,它也是Java语言的扩展.groovy中具有很多脚本语言的共性(比如弱类型, 闭包等等), groovy中存在两个看起来类似的概念,即是方法与闭包.因此本文总结了groovy中方法与闭包的共同点与区别.

不同点

方法与闭包作用域问题

首先看如下的情况

def val = "hello"
def test_func() {
    println val
}
test_func()

按照以往脚本的编程经验, 上述函数可以正确的输出结果, 然而在groovy中却报错

groovy.lang.MissingPropertyException: No such property: val for class: ConsoleScript42

定义的test方法不可以获取到方法外定义的val 变量.原因是val 的作用域不包含test_func方法. 理解这一点需要了解groovy的一种运行方式. Groovy本身也是运行于JVM中, 它需要被编译成class字节码文件运行, 当我们将上述的groovy脚本编译成class文件, 然后通过反编译工具查看反编译的Java文件大致如下:

public class test{
	public static void main(String... args){
		// call run
	}
	public Object run(){
	}
	public Object test_func(){
	}
}

可以看到创建的Java 类中会有一个run方法, 这个run方法的作用域其实就是编写groovy脚本的顶层属性的scope(val 变量所在的scope), 而test_func这个方法只是类中的一个方法而已,它和run方法是并列的两个不同的方法,作用域不同自然不能够相互访问其中的属性.

方法递归与闭包递归

一个简单的递归例子, 比如使用递归的方法求解一个list中的最大值, 使用函数的代码实现如下:

def max_val(list, val) {
    if(list.size() == 1) return list[0]>val?list[0]:val
    else return max_val(list[1..<list.size()], list[0]>val?list[0]:val)
}

与其他脚本语言一样,比较简单, 但是如果使用闭包实现, 代码如下:

def max_val_clo = {list,val->{
        if(list.size() == 1) return list[0]>val?list[0]:val
        else return call(list[1..<list.size()], list[0]>val?list[0]:val)
    }
}

关键的就是在闭包里面使用call 指代本身,一些教程里面在这里实现递归可能会用闭包的名字(此处是max_val_clo)或者是this.call, 但是在最新的groovy中, 这些语法都会出错. 在初次接触groovy的闭包的时候就了解到闭包可以使用call方法接受参数运行,如下:

def clo = {val -> println "hello ${val}"}
clo("world")
clo.call("world")

所以在闭包中使用递归则用call 直接表示闭包本身,不需要其他额外的语法.

重载问题

函数支持重载而闭包没有重载这个概念.如下一个简单的方法重载:

def func(arg1, arg2) {
    arg1*arg2
}

def func(arg1){
    arg1*2
}

println "${func(2, 3)}"
println "${func(4)}"

Groovy中定义的函数重载是参数数量不同函数名称相同,实际上如果函数的形参类型不同也可以重载,比如如下的函数使用了静态的参数类型就可以重载

def func(String arg1) {
    arg1*2
}

def func(Number arg1){
    arg1*2
}

println "${func(2)}"
println "${func('hello')}"

然而闭包就不能够进行重载

def clo = {
    arg -> arg*3
}
def clo = {
    arg1,arg2 -> arg1*arg2
}
println "${clo(2)}"

运行报错

The current scope already contains a variable of the name clo

其实这个问题非常好理解, 闭包一般需要与一个闭包变量关联,上述的错误其实就是重复定义变量的错误, 所以闭包里面不存在重载这个说法.

闭包中特有的关键字

闭包中有四个关键字需要辨别清楚,四个关键字分别是it this owner delegate 其中it 是closure中的隐含参数, 当closure中不明确定义参数列表的时候, 其实可以使用其内部隐藏的参数it, 比如:

def clo = {
    println "hello $it"
}

clo("world")

注意这个it 一定是没有指定参数列表的时候有效, 如果已经指定了参数列表,那么这个it 就无效了, 比如:

def clo = {item ->
    println "hello $it"
}

clo("world")

运行出错, 错误是:

groovy.lang.MissingPropertyException: No such property: it for class: groovythis

总结就是当closure最多只需要一个参数的时候,可以不显示定义参数列表,在closure的body里面直接使用隐藏的参数it 即可获取形参的值.
this 关键字:这个关键字和method 中的this的意义是一样的,表示定义这个closure的直接的class的对象实例, 但是由于closure本身比较灵活,容易混淆.

class Enclosing {
    void run() {
        def clo = {
            getThisObject()
            // closure 中调用getThisObject方法
            // 返回的是封装它的类的实例
        }
        assert clo() == this
        def clo2 = {this} // closure 中
        // 的this 关键字依然返回的是定义时包装它的类的实例
        // this instanceof Enclosing
        assert clo2() == this
    }
}
new Enclosing().run()

class EnclosedInnerClass {
    class Inner {
        def cl = {this}
        // 内部类中定义的closure中的this 返回的是
        // 内部类的实例 this instanceof EnclosedInnerClass
    }
    
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
    }
}
new EnclosedInnerClass().run()

class NestedClosures {
    void run(){
        def nestedClosures = {
            def cl = {this}
            cl()
        }
        assert nestedClosures() == this
        // 嵌套闭包中的this 返回的依然是包装它的类的实例
        // 而不是包装它的闭包对象 this依然instanceof NestedClosures
    }
}
new NestedClosures().run()

以上例子中包含了所有的closure中this会使用到的情况,总之无论是内部类中的closure, 还是嵌套的closure,其中的this 都是直接包含它的类的实例对象.
owner 关键字:它与this 相似, 区别是 this 指的是类的实例, 而owner 指的是包含定义这个closure的对象, 可以是一个类的实例也可以是一个closure. 也就是说如果这个closure直接定义在一个class里面,那么thisowner 指的是同一个对象(class的实例). 但是如果这个closure嵌套在另外一个closure里面那么owner 返回的是这个外层的closure对象,而this 返回的是顶层的class对象实例.closure中调用getOwner() 方法可以返回这个owner. 如下的例子:

class Enclosing {
    void run() {
        def clo = {
            getOwner()
            // closure 中使用getOwner 返回owner对象
            // 此时是一个class直接包装了closure
            // 返回值即是这个class的实例
        }
        assert clo() == this
        def clo2 = {owner}
        //此时this 和 owner指代的都是这个class的实例
        assert clo2() == this
    }
}
new Enclosing().run()

class EnclosedInnerClass {
    class Inner {
        def cl = {owner}
        // 内部类中定义的closure返回的owner 依然是这个内部类的实例
    }
    
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
    }
}
new EnclosedInnerClass().run()

class NestedClosures {
    void run(){
        def nestedClosures = {
            def cl = {owner}
            cl()
        }
        assert nestedClosures() == nestedClosures
        // 这是与this最本质的区别 owner可以返回class实例对象或者closure对象,但是是最直接的对象,这个地方显然nestedClosures是直接包含的对象
        // 所以owner指代的值是nestedClosures
    }
}
new NestedClosures().run()

delegate 关键字: closure中最有意思的一个属性,默认的初始值和owner 一样, 但是delegate 关键字是用户可自定义的, 也就是说可以人为改变这个属性的引用对象,以及在这个属性上面添加额外的用户自定义属性,这个关键字相当于给closure 添加了额外的scope

class Test {
    void run(){
        def clo1 = {
            delegate
        }
        def clo2 = {
            getDelegate()
        }
        def clo3 = {
            owner
        }
        assert clo1() == clo2()
        assert clo1() == clo3()
        // closure中使用getDelegate() 方法可以返回这个delegate对象
        // 默认的delegate值是owner 所以第二个断言返回为true
        def nestedClosure = {
            {-> delegate}.call()
            // 此时的返回值是外层的nestedClosure 等于owner
        }
        assert nestedClosure() == nestedClosure
    }
}

new Test().run()

resolveStrategy 可以简单理解为closure中的属性访问的策略,是closure中的精华所在,在closure中涉及到属性的访问的时候可以不明确的指定属性存在的对象,举个例子:

class Test{
    def name
}

def clo = {
    name.toUpperCase()
}

def test = new Test(name:'Kris')
clo.delegate = test
println clo() // 成功输出KRIS

也就是说当我们在closure中访问属性的时候是可以需要以属性绑定者.属性名称 的方式访问, 因为closure本身有一个机制就是在所有可能的属性绑定者中查询到需要访问的属性, 默认的查询链即是owner->delegate 也就是说当我们在closure中访问一个属性的时候首先从包装这个closure的类实例中查找,如果没有再从包装这个类的对象(可能是类实例也可能是另外一个closure)中查找,最后从delegate 中查找. 官方提供了五种:1.Closure.OWNER_FIRST 默认的访问策略,先从owner中查找属性, 然后从delegate中查找属性;2.Closure.DELEGATE_FIRST 与上面相反, 先从delegate查找属性,再从owner中查找属性;3.Closure.OWNER_ONLY 只在owner中查找属性,delegate会被忽略;4.Closure.DELEGATE_ONLY 只在delegate中查找属性, owner会被忽略;
5.Closure.TO_SELF 理解为自定义的访问策略,只是在实现Closure子类的时候使用.举个例子:

class Test{
    def name
    def clo = {
        name
    }
}

class A {
   def name
}
def a = new A(name:"Kris")
def test = new Test(name:"Wu")
test.clo.delegate = a // 如果使用默认的访问优先级为owner所以返回值为Wu
test.clo.resolveStrategy = Closure.DELEGATE_FIRST // 设置优先从delegate访问
assert test.clo() == 'Kris'

当然使用ONLY型的访问策略,如果找不到指定的属性则会抛出groovy.lang.MissingPropertyException异常, 比如:

class Test{
    def name
    def age
    def clo = {
        age
    }
}

class A {
   def name
}
def a = new A(name:"Xiaoming")
def test = new Test(name:"Wu", age:20)
test.clo.delegate = a
assert test.clo() == 20
test.clo.resolveStrategy = Closure.DELEGATE_ONLY
// age 属性不在delegate里面 所以会抛出属性不存在的异常
try{
test.clo()
}catch(MissingPropertyException ex){
}

相同点

静态类型支持

Groovy同其他的脚本语言一样是弱类型的语言, 声明变量的时候不需要指明类型, 同样定义函数或者闭包的时候也不需要指明变量的类型.但是有时候为了让调用更加的安全, 也可以指定参数或者变量的类型.

def func1(arg1, arg2) {
    arg1*arg2
}

println "${func1(2, 3)}"
println "${func1(3.5, 2)}"
println "${func1('test', 3)}"

运行结果:

6
7.0
testtesttest

一个简单的乘法运算如果不限定参数类型可能会得到一些意想不到不到的结果, 所以当我们的函数或者闭包需要更安全的调用时,还是需要指定参数的类型以及返回值类型

def func2(Number arg1, Number arg2) {
    arg1*arg2
}

def clo = {
    Number arg1, Number arg2 -> arg1*arg2
}

println "function result is ${func2(1, 2)}"
println "closure result is ${clo(1,2)}"
println "function result is ${func2('test', 2)}"
println "closure result is ${clo('test', 2)}"

后两行的运行会报错, 与预想的一样,输入类型不支持字符串类型

ConsoleScript1.func2() is applicable for argument types: (String, Integer) values: [test, 2]
Possible solutions: func2(java.lang.Number, java.lang.Number)

默认参数支持

闭包与函数都支持默认参数, 且默认参数都放置于参数列表的最后

def func(arg1, arg2='world') {
    println "${arg1} ${arg2}"
}

def clo = {
    arg1, arg2='world' -> println "${arg1} ${arg2}"
}

func('hello')
clo('hello')
func('nihao')
clo('nihao')

运行结果:

hello world
hello world
nihao world
nihao world

小结

本文主要分析了一下Groovy中函数与闭包的相同点与不同点,这些只是在工作和学习过程中的一些比较浅显的总结,如果后续更加深入的了解了Groovy语言以及函数与闭包的其他特点,还会继续更新本文.

<think>我们正在构建一个基于Python的接口自动化测试框架。根据引用[1]引用[2],我们可以设计一个结构清晰的框架。以下是一个详细的设计与实现步骤: ### 1. 框架结构设计 参考引用[1]的目录结构,我们可以设计如下: ``` api_framework/ # 项目根目录 ├── common/ # 公共模块 │ ├── __init__.py │ ├── base.py # 基础类,如请求封装 │ ├── config.py # 配置文件读取 │ ├── logger.py # 日志模块 │ └── request.py # 请求模块 ├── testcases/ # 测试用例 │ ├── __init__.py │ ├── test_login.py # 登录测试用例 │ └── test_order.py # 订单测试用例 ├── data/ # 测试数据(如Excel、JSON等) │ └── test_data.xlsx ├── reports/ # 测试报告 ├── utils/ # 工具类 │ ├── __init__.py │ ├── excel_util.py # Excel操作工具 │ └── assert_util.py # 断言工具 ├── conftest.py # pytest配置文件(钩子函数、夹具等) ├── pytest.ini # pytest配置文件 └── run.py # 主运行入口 ``` ### 2. 核心模块实现 #### (1) 配置文件管理 (config.py) 使用`configparser`读取配置文件,例如`config.ini`。 ```python # common/config.py import os import configparser class Config: def __init__(self, config_file='config.ini'): self.config = configparser.ConfigParser() self.config.read(os.path.join(os.path.dirname(__file__), config_file)) def get(self, section, option): return self.config.get(section, option) # 实例化,方便导入 config = Config() ``` #### (2) 日志模块 (logger.py) 使用`logging`模块记录日志。 ```python # common/logger.py import logging import os from common.config import config # 从配置中读取日志级别 log_level = config.get('LOG', 'level').upper() log_path = config.get('LOG', 'path') def setup_logger(): logger = logging.getLogger('api_test') logger.setLevel(log_level) # 创建文件处理器 if not os.path.exists(log_path): os.makedirs(log_path) file_handler = logging.FileHandler(os.path.join(log_path, 'test.log')) file_handler.setLevel(log_level) # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(log_level) # 创建格式化器 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 logger.addHandler(file_handler) logger.addHandler(console_handler) return logger logger = setup_logger() ``` #### (3) 请求封装 (request.py) 使用`requests`库发送HTTP请求,并集成日志记录。 ```python # common/request.py import requests from common.logger import logger class Request: def __init__(self): self.session = requests.Session() def request(self, method, url, **kwargs): try: response = self.session.request(method, url, **kwargs) logger.info(f"Request: {method} {url} - Status: {response.status_code}") return response except Exception as e: logger.error(f"Request Error: {e}") raise # 实例化 request = Request() ``` #### (4) 测试用例示例 (test_login.py) 使用`unittest`编写测试用例,并利用封装的请求类。 ```python # testcases/test_login.py import unittest from common.request import request class TestLogin(unittest.TestCase): def test_login_success(self): url = "https://example.com/api/login" data = {"username": "admin", "password": "123456"} response = request.request("POST", url, json=data) self.assertEqual(response.status_code, 200) self.assertIn("token", response.json()) ``` ### 3. 数据驱动 使用`ddt``unittest`结合,从Excel中读取测试数据。 #### (1) Excel工具类 (excel_util.py) ```python # utils/excel_util.py import openpyxl def read_excel(file_path, sheet_name): workbook = openpyxl.load_workbook(file_path) sheet = workbook[sheet_name] rows = list(sheet.iter_rows(values_only=True)) headers = rows[0] data = [] for row in rows[1:]: data.append(dict(zip(headers, row))) return data ``` #### (2) 使用ddt驱动测试用例 ```python # testcases/test_login_ddt.py import unittest from ddt import ddt, data, unpack from utils.excel_util import read_excel from common.request import request @ddt class TestLoginDDT(unittest.TestCase): @data(*read_excel('data/test_data.xlsx', 'login')) @unpack def test_login(self, username, password, expected_code, expected_msg): url = "https://example.com/api/login" data = {"username": username, "password": password} response = request.request("POST", url, json=data) self.assertEqual(response.status_code, expected_code) self.assertEqual(response.json()['msg'], expected_msg) ``` ### 4. 测试报告 使用`HTMLTestRunner`生成测试报告,或使用`pytest-html`插件(如果使用pytest)。 #### 运行入口 (run.py) ```python import unittest from HTMLTestRunner import HTMLTestRunner # 加载测试用例 discover = unittest.defaultTestLoader.discover('testcases', pattern='test_*.py') # 运行测试并生成报告 with open('reports/report.html', 'wb') as f: runner = HTMLTestRunner( stream=f, title='接口自动化测试报告', description='测试结果' ) runner.run(discover) ``` ### 5. 持续集成 可以将框架集成到Jenkins等持续集成工具中,实现定时执行或触发执行。 ### 总结 以上是一个基本的接口自动化测试框架的设计与实现。主要包含: - 公共模块:配置、日志、请求封装 - 测试用例:使用unittestddt实现数据驱动 - 测试数据:从Excel读取 - 测试报告:使用HTMLTestRunner生成 ### 优化方向 1. **增加异常处理**:增强框架的健壮性。 2. **数据库支持**:对于复杂系统,测试数据可以存储在数据库中。 3. **接口依赖处理**:实现接口之间的参数传递(如token)。 4. **使用pytest**:提供更丰富的插件支持(如pytest-html, pytest-xdist分布式执行)。 5. **加入Mock服务**:用于依赖外部接口的场景。 §§相关问题§§ 1. 如何实现接口自动化测试框架中的接口依赖(如获取token后用于后续请求)? 2. 如何使用pytest重构这个框架并利用其插件? 3. 在接口测试中如何处理文件上传下载? 4. 如何将接口自动化测试框架集成到Jenkins中?</think>### 基于Python的接口自动化测试框架设计与实现 #### 1. 框架核心设计原则 - **模块化设计**:分离测试逻辑、请求封装、数据管理报告生成 - **可扩展性**:支持插件机制,便于集成数据库、消息队列等组件 - **数据驱动**:实现测试数据与脚本分离(支持Excel/JSON/YAML) - **日志监控**:全过程日志跟踪,支持多级别日志输出 - **异常处理**:完善的错误重试断言机制 #### 2. 框架分层架构 ```markdown api_auto_framework/ ├── core/ # 核心引擎 │ ├── request_client.py # 请求封装(支持HTTP/HTTPS) │ ├── assertion.py # 断言机制(支持JSON Schema) │ └── test_runner.py # 测试执行器 ├── testcases/ # 测试用例层 │ ├── auth/ # 按业务模块组织 │ │ └── test_login.py │ └── order/ │ └── test_order.py ├── data/ # 测试数据管理 │ ├── test_data.yaml # 数据驱动文件 │ └── schema/ # JSON Schema定义 ├── utils/ # 工具库 │ ├── config_loader.py # 配置加载 │ ├── logger.py # 日志系统 │ └── db_connector.py # 数据库连接 ├── reports/ # 测试报告 │ └── html_report.html # 可视化报告 └── conftest.py # 全局夹具配置 ``` #### 3. 核心模块实现(代码示例) ##### (1) 请求封装 (`request_client.py`) ```python import requests from utils.logger import logger class RequestClient: def __init__(self, base_url): self.session = requests.Session() self.base_url = base_url self.timeout = 10 # 默认超时时间 def _send_request(self, method, endpoint, **kwargs): url = f"{self.base_url}{endpoint}" try: response = self.session.request( method, url, timeout=self.timeout, **kwargs ) logger.info(f"请求成功: {method} {url} - 状态码: {response.status_code}") return response except requests.exceptions.RequestException as e: logger.error(f"请求失败: {e}") raise def get(self, endpoint, params=None): return self._send_request('GET', endpoint, params=params) def post(self, endpoint, json=None, data=None): return self._send_request('POST', endpoint, json=json, data=data) ``` ##### (2) 数据驱动测试用例 (`test_login.py`) ```python import pytest from core.request_client import RequestClient from utils.config_loader import config @pytest.mark.parametrize("test_data", config.load_test_data("login_cases.yaml")) def test_login(test_data): client = RequestClient(config.BASE_URL) response = client.post( "/api/login", json={ "username": test_data["user"], "password": test_data["password"] } ) # 断言状态码响应结构 assert response.status_code == test_data["expected_code"] assert "token" in response.json() ``` ##### (3) 配置管理 (`config_loader.py`) ```python import yaml import os class ConfigLoader: def __init__(self): self.config = self._load_config("config.yaml") def _load_config(self, filename): with open(os.path.join("config", filename), encoding='utf-8') as f: return yaml.safe_load(f) @property def BASE_URL(self): return self.config["api"]["base_url"] def load_test_data(self, filename): with open(os.path.join("data", filename), encoding='utf-8') as f: return yaml.safe_load(f)["test_cases"] # 全局配置实例 config = ConfigLoader() ``` #### 4. 关键优化策略 1. **动态参数处理** - 使用`jinja2`模板实现参数化:`{{timestamp}}` → 自动替换为当前时间戳 - : ```python from datetime import datetime def render_template(data): if "{{timestamp}}" in data: return data.replace("{{timestamp}}", str(int(datetime.now().timestamp()))) return data ``` 2. **自动化关联处理** - 上下文管理器存储跨接口数据: ```python class TestContext: def __init__(self): self._store = {} def set(self, key, value): self._store[key] = value def get(self, key): return self._store.get(key) ``` 3. **智能断言机制** - 支持多种断言类型: ```python def assert_response(response, validations): for val in validations: if val["type"] == "status_code": assert response.status_code == val["expected"] elif val["type"] == "json_path": assert jsonpath(response.json(), val["path"]) == val["expected"] ``` #### 5. 持续集成集成方案 1. Jenkins Pipeline配置示例: ```groovy pipeline { agent any stages { stage('执行测试') { steps { sh 'pytest --alluredir=./reports' } } stage('生成报告') { steps { allure serve './reports' } } } } ``` #### 6. 框架扩展建议 - **性能监控**:集成`locust`实现压力测试 - **自动生成文档**:通过`pytest`钩子自动生成OpenAPI文档 - **消息通知**:添加企业微信/钉钉测试结果推送 - **容器化部署**:使用Docker封装测试环境 > 最佳实践:优先实现核心业务流程的覆盖,逐步扩展异常场景性能测试能力[^1][^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值