前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >线程的复用---线程池原理解析

线程的复用---线程池原理解析

作者头像
Java进阶之路
发布于 2022-08-03 09:06:17
发布于 2022-08-03 09:06:17
41000
代码可运行
举报
运行总次数:0
代码可运行

一:简述

本文基于java11对线程池的参数,执行任务的流程以及原理进行解析,并且对线程池关键性源码进行了分析。

二:线程池的参数

1. corePoolSize: 用于指定线程池的核心线程数量

2. maximumPoolSize: 用于定义线程的最大线程数

3. keepAliveTime: 非核心线程的空闲时间

4. timeUnit: 空闲时间单位

5. blockingQueue: 阻塞队列

6. threadFactory: 线程工厂,可以用于定义线程的名称,线程的优先级,是否是守护线程等信息,支持自定义线程工厂,默认采用DefaultThreadFactory

7. rejectedExecutionHandler: 拒绝策略,jdk已经实现了四种拒绝策略

a. AbortPolicy 直接抛出异常,也是默认的拒绝策略

b. CallerRunsPolicy 使用当前线程执行任务

c. DiscardOldestPolicy 丢弃最早加入的任务,并执行任务

d. DiscardPolicy 直接丢弃当前任务

三:Java提供的四种线程池

在Executors工厂类中,Java默认提供了四种类型的线程池。

1.newFixedThreadPool:有固定的线程数的线程池,最大线程数等于核心线程数,阻塞队列使用LinkedBlockingQueue,线程空闲时间为0

2.newCachedThreadPool:可缓存的线程的线程池,核心线程数为0,最大线程数为Integer.MAX,线程空闲时间为60s,阻塞队列使用SynchronousQueue

3.newScheduledThreadPool:能够定时完成任务的线程池,核心线程数自定义,最大线程数为Integer.MAX,线程空闲时间为0,阻塞队列为DelayedWorkQueue

4.newSingleThreadExecutor:单线程的线程池,核心线程数和最大线程数都为1,阻塞队列为LinkedBlockingQueue,线程空闲时间为0

四:线程池状态

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    //运行状态
    private static final int RUNNING    = -1 << COUNT_BITS;
    // shutdown状态 不接收新任务,但能处理已添加的任务
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //停止状态 不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    private static final int STOP       =  1 << COUNT_BITS;
    //当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时 会由SHUTDOWN状态扭转为TIDYING
    private static final int TIDYING    =  2 << COUNT_BITS;
    //线程池彻底终止,就变成TERMINATED状态
    private static final int TERMINATED =  3 << COUNT_BITS;

五:线程池执行任务流程图

六:源码解析

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ThreadPoolExecutor threadPool = new
                ThreadPoolExecutor(4, 8, 60,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.AbortPolicy());
        threadPool.execute(()->{
            System.out.println("线程池原理解析");
        });

execute()方法

execute()方法作为阅读相关源码的入口,首先对execute()方法进行分析,从execute()方法可以看出线程池的主流程,工作线程数量没有达到核心线程数的话会优先创建线程并执行任务,达到核心线程数量之后,将任务放入到阻塞队列中,阻塞队列满了之后继续创建线程执行任务,直到工作线程数量达到最大线程数,再执行拒绝策略。其中包含两个比较重要的方法 addWorker(),reject()方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
  //c的前3位表示线程池的状态 后29位表示线程的数量   
        int c = ctl.get();
  //如果当前线程数量小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
  //调用addWorker添加一个线程并执行任务 
  //addWorker在线程池状态不是running 或者 工作线程数量超过限制返回false
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
  //走到下面的逻辑 addworker是添加非核心线程了 也就是第二个参数为false
  //如果线程池是运行状态 通过offer方法把任务放入阻塞队列
  //offer方法放入队列成功返回true 失败返回false
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
      //进一步判线程池状态 不是运行状态 而且移除任务成功
            if (! isRunning(recheck) && remove(command))
    //执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
    //如果工作线程数量为0 那么重新添加非核心工作线程去执行任务 因为已经把任务加入到阻塞队列中了 而工作线程为0 所以需要addWorker()添加工作线程执行任务
                addWorker(null, false);
        }
  //表示加入队列失败 那么addworker 新增一个非核心工作线程
  //返回false 表示线程池状态不是running 或 工作线程数量超过限制
        else if (!addWorker(command, false))
      //执行拒绝策略
            reject(command);
    }

接下来分别针对addWork()方法和reject()方法进行分析

addWorker()

addWorker()方法会检验线程池的状态,如果不是运行状态,直接返回false,根据core标志判断工作线程是否达到线程限制(参数core为true时,不能超过核心线程数,为fasle时不能超过最大线程),没有达到限制的话创建一个worker对象,而worker对象是一个线程,进一步判断线程池状态,如果是运行状态,那么调用start()方法启动worker线程,启动之后肯定会执行Worker对象中的run()方法。而如果启动线程失败,会调用addWorkerFailed()方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
      //判断线程池状态 如果已经不是运行状态 那么直接返回false
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
      //校验当前线程数量
      //如果core标识为true 那么如果大于核心线程数就返回fasle  
      //如果core标识为false,大于最大线程数返回false
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
    //通过cas新增线程数量 新增成功跳出自旋  
                if (compareAndIncrementWorkerCount(c))
    //跳出retry指定的for循环 在这里也就是跳出了外面的for循环了
                    break retry;
    //重新获取c的值 因为如果上面cas失败了 那么c的值肯定是变动了 所以重新获取一下
                c = ctl.get();  // Re-read ctl
    //如果线程池的状态已经是shoudown 或 stop等非运行的状态 那么跳过这次的循环
                if (runStateAtLeast(c, SHUTDOWN))
        //重新进入到外面的for循环
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
  //工作线程是否启动的标识
        boolean workerStarted = false;
  //工作线程是否添加成功的标识
        boolean workerAdded = false;
        Worker w = null;
        try {
      // work实现了Runnable 是一个线程 这里只是创建一个线程 还没有开始运行
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
    //加锁
                mainLock.lock();
                try {

                    int c = ctl.get();
        //判断线程池的状态
                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null)) {
      //当前的工作线程不是新生状态 抛出异常
                        if (t.getState() != Thread.State.NEW)
                            throw new IllegalThreadStateException();
      //把woker对象放入workers中 这是一个HashSet
                        workers.add(w);
      //修改工作线程是否添加成功的标识
                        workerAdded = true;
            
                        int s = workers.size();
                        if (s > largestPoolSize)
          //更新largestPoolSize 用于统计线程池的最大size 用于监控数据
                            largestPoolSize = s;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
        //如果已经添加成功 那么启动工作线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
    //启动失败 那么处理失败的情况
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorkerFailed()

启动线程失败时调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
  //加锁
        mainLock.lock();
        try {
            if (w != null)
    //从set中移除启动失败的woker
                workers.remove(w);
      //将工作线程数减一 已经加锁 不需要通过cas来修改了   
            decrementWorkerCount();
      //根据线程池的状态看是否需要停止线程池
            tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

可以看出worker类实现了Runnable接口,是一个线程,并且在构造函数中将任务赋值给firstTask成员变量,并且通过线程工厂将当前worker对象封装之后保存在thread成员变量中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
      
        final Thread thread;

        Runnable firstTask;

        volatile long completedTasks;
        
       Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }
}

接下来分析worker对象中的run()方法,run()方法会调用runWorker()方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void run() {
    runWorker(this);
}复制代码

runWorker()

要使工作线程能够复用,所以run()方法不能结束,所以采用while()循环来保证有任务的情况下run()方法是不会结束的。firstTask为空的情况下,在调用getTask()获取到任务之后会执行任务的run()方法,注意不是执行start()方法,因为当前的worker对象已经是一个线程,所以直接调用任务的run()方法执行里面的逻辑就行了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
       //这里是while循环 用来保证有任务的时候run方法不执行结束
      //while这里的判断也就是说 task为空而且获取getTask()返回为空 那么线程就会结束
      //注意 这里是短路或 说明如果woker对象中的firstTask不为空 就不会调用getTask()
      //所以getTask()返回null 意味着结束当前woker线程
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
    //判断线程池状态 如果线程池已经停止 那么任务线程中断
    //如果不是 那么确保线程不中断
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
         //执行任务之前的处理  默认是空实现 
        //继承ThreadPoolExecutor 重写方法 可以加入自己的逻辑
                    beforeExecute(wt, task);
                    try {
      //调用任务的run方法 注意这里是调用传入的任务的run方法 
      //因为已经创建了work线程来执行 所以不是调用start()方法执行任务
                        task.run();
      //执行任务之后的处理  默认是空实现 
      //继承ThreadPoolExecutor 重写方法 可以加入自己的逻辑
                        afterExecute(task, null);
                    } catch (Throwable ex) {
                        afterExecute(task, ex);
                        throw ex;
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

接下来分析getTask()方法

getTask()

通过从阻塞队列中获取任务,如果当选线程数量大于核心线程那么通过poll(timeout,TimeUnit)来阻塞工作线程,否则直接调用take()方法一直阻塞,直到获取到任务,然后返回。(注意:从这里我们可以知道,所谓的核心线程和非核心线程不是线程的属性,线程池只要维护核心线程的数量即可)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Runnable getTask() {
  // 用来表示上一次循环的时候,非核心线程获取任务是否超时
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();

            // Check if queue empty only if necessary.
      // 如果线程池已经不是running状态 而且队列是空的 
      //那么将工作线程数量减1并返回null 也就是结束当前work线程
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
      //获取工作线程数量
            int wc = workerCountOf(c);

            // Are workers subject to culling?
      //allowCoreThreadTimeOut 默认为false 标识核心线程在空闲时也不回收
      //所以这里也就是工作线程数大于核心线程数的时候 timed为true
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            //这个判断标识如果工作线程数已经大于最大线程数并且大于1 而且队列已经是空
      //(如果上一次循环的时候,非核心线程获取任务超时,那么timedOut为true)
      //那么将cas将工作线程数减1 并返回null 结束当前work线程
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
    //如果cas失败 那么证明工作线程数已经变化 重新进入for循环  
                continue;
            }

            try {
    //工作线程数大于核心线程数的时候 timed为true
    //从阻塞队列中获取任务 如果工作线程数大于核心线程数 那么利用配置的最大空闲时间和时间单位对线程进行有时间限制的阻塞 否则调用take阻塞
    //从这里我们可以知道工作线程数小于核心线程数是不会回收的 一直阻塞在这
    //而超过核心线程数 那么需要根据配置空闲时间来回收线程 
    //poll(timeout,unit)超时没有获取到会返回null,而take()方法会一直阻塞在这 知道获取到阻塞队列中的任务
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
    //获取到任务直接返回 从这里可以知道非核心线程也不是超时了就结束掉(并没有返回null,),而是进入下一次循环 还有一次获取任务的机会
                if (r != null)
                    return r;
    //否则将timedOut设置为true 进入下一次循环
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

addWorker()的部分分析完之后,接下来分析reject()方法

reject()

reject()方法很简单,根据构造函数传过来的拒绝策略调用相应的rejectedExecution()方法(策略模式)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
final void reject(Runnable command) {
   //根据拒绝策略的参数分别执行相应的操作
   handler.rejectedExecution(command, this);
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 1. 当前线程执行任务
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

//2.默认 直接抛出异常
public static class AbortPolicy implements RejectedExecutionHandler {

        public AbortPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

//3.什么都不做
public static class DiscardPolicy implements RejectedExecutionHandler {

        public DiscardPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

//4.丢弃队列头部的任务,也就是最旧的任务,并将当前任务调用execute()
public static class DiscardOldestPolicy implements RejectedExecutionHandler {

        public DiscardOldestPolicy() { }


        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

七:结束语

线程池在我们工作中,使用的场景还是比较多的,而且线程是比较重要的资源,所以理解线程池的原理,合理利用线程池是很有必要的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java进阶之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
译文: iOS Unit Testing and UI Testing Tutorial
原文: iOS Unit Testing and UI Testing Tutorial,作者:Audrey Tam。更新于2017年3月13日。以下为正文:
iOS Development
2019/02/14
1.4K0
LLT工作总结与Gherkin语法解析器简单应用
这几天产品线这里要搞LLT(Low level Test)重点工作,保障版本的高质量发布。工作当然包括一系列的规范、培训、编码、检视,不过具体看下来主要还是提取了下面的一些度量要点:
mythsman
2022/11/14
9370
LLT工作总结与Gherkin语法解析器简单应用
【腾讯 TMQ 】移动 APP 自动化测试框架对比
作者:赵丽娜 简介 移动 APP 的 UI 自动化测试长久以来一直是一个难点,难点在于UI的”变”, 变化导致自动化用例的大量维护。 从分层测试的角度,自动化测试应该逐层进行。 最大量实现自动化测试的
腾讯移动品质中心TMQ
2017/03/06
6.6K0
使用Calabash进行Android和iOS UI测试
因为各种事,这篇本来属于上周的拖到了就今天,一篇关于移动端测试工具Calabash的文章,看着篇幅比较小,就接受了。本身精力不在Android和iOS开发,所以也就没按部就班的复原教程中的实例,仅当开阔视野了。
WindCoder
2018/09/19
2.1K0
使用Calabash进行Android和iOS UI测试
分层测试
自动化测试一直是测试领域桂冠上的明珠,几乎所有的测试团队都有建立团队的自动化。测试团队的自动化建设也被认为是团队提效的必经之路,但搭建和使用自动化路但路却并非一帆风顺。搭建自动化但时候有很多框架可以选用,合理但选择适合该团队的框架可以事半功倍,同时选择了框架之后就要受制于框架。使用自动化很多时候因为学习以及维护成本高,让初衷是提效为目的的自动化,成为了加重测试工作量之殇。
luxididi
2020/06/14
5.8K1
史上最全的 iOS 各种测试工具集锦!
随着移动互联网的兴起,APP 测试的越来越被重视!Android 系统因为自己的开源性,测试工具和测试方法比较广为流传,但是 iOS 系统的私密性,导致很多测试的执行都有点麻烦。
测试开发技术
2024/03/22
6900
史上最全的 iOS 各种测试工具集锦!
Cucumber测试实践
来源:https://cucumber.io/docs/guides/overview/
ThoughtWorks
2022/02/16
9550
Cucumber测试实践
两款iOS和Android都通用的开源自动化测试工具
自动化测试在产品测试上有着非常重要的作用。实现测试自动化有多种积极的方式,包括最大限度地减少测试执行时间;在关键的发布阶段,用更少的时间确保更大的覆盖范围;在产品开发阶段,可靠又重复性地运行以确保没有
非著名程序员
2018/02/02
2.9K0
两款iOS和Android都通用的开源自动化测试工具
Android 平台实现 CI
在TW技术雷达上,有一个很重要的趋势,就是把在传统技术领域已经成熟的技术实践,比如持续集成,推广到新兴技术领域,比如移动开发和前端开发。 北京办公室的朱傲这次用自己的实践告诉我们如何在Android这
ThoughtWorks
2018/04/18
1.8K0
Android 平台实现 CI
客户端自动化测试研究
背景 测试作为质量保证极其重要的一环,在移动App开发流程中起到非常关键的作用。从开发工程师到测试工程师,人人都应具备良好的测试意识,将隐患和风险在上线之前找出并解决,可以有效的减少线上事故。 美团和大众点评App作为美团点评平台的主要入口,支持承载着美团点评各大业务。其中美团点评境外度假业务主要包括了出境游相关业务以及所有的境外城市站,也是美团点评非常看重和大力发展的业务线。为了保证质量,需要进行各项测试:冒烟测试[1]、功能测试、集成测试、专项性能测试,回归测试[2]。其中冒烟测试和回归测试大多由开发自
美团技术团队
2018/03/13
3.3K0
客户端自动化测试研究
软件测试|iOS 自动化测试——技术方案、环境配置
移动端的自动化测试,最常见的是 Android 自动化测试,我个人觉得 Android 的测试优先级会更高,也更开放,更容易测试;而 iOS 相较于 Android 要安全稳定的多,但也是一个必须测试的方向,这个系列文章记录了 iOS 自动化测试的一些实践。
霍格沃兹测试开发Muller老师
2022/12/29
1.5K0
你不知道的Cypress系列(1) --鸡肋的BDD
Behavioural Driven Development (BDD)是从TDD发展来的(什么,TDD你都不知道?!),它通过自然语言定义系统行为,以功能使用者的角度,编写需求场景,且这些行为描述可以直接形成需求文档,同时也是测试标准。
iTesting
2020/12/15
1.6K0
你不知道的Cypress系列(1) --鸡肋的BDD
QA应该更新的测试工具
视觉感知测试,对于很多 QA,包括我在 2013 以前对于它的认知都是手动测试领域的一个成员。在这个 Web 系统爆炸的年代,Web UI 界面布局测试,多浏览器测试,CSS 的 refactor 等都成为了 Web UI 测试的痛中之痛,特别是大型 Web 应用的功能回归测试量太大,从而导致很多时候根本无法完成,所以很少会有团队去做全方位的 UI 界面布局回归测试,特别是对于使用 Agile 流程开发的团队就更加困难。
Criss@陈磊
2020/04/26
1.7K0
移动开发流水线建立以及自动化测试
工业时代流水线的发明将生产任务的效率大大提升。同样,在软件开发过程中流水线的建立也能帮助我们更好的产出、提升效率。
100000798482
2018/08/20
1.3K0
移动开发流水线建立以及自动化测试
iOS 自动化测试踩坑(一): 技术方案、环境配置与落地实践
移动端的自动化测试,最常见的是 Android 自动化测试,我个人觉得 Android 的测试优先级会更高,也更开放,更容易测试;而 iOS 相较于 Android 要安全稳定的多,但也是一个必须测试的方向,这个系列文章记录了 iOS 自动化测试的一些实践。
霍格沃兹测试开发
2022/05/06
1.4K0
如何高效使用Gherkin
时间回到2022年,我参与了一个使用了Flutter技术构建的Web前端项目。在这个项目上,我们小组的目标是实施Flutter前端自动化测试。
ThoughtWorks
2023/09/18
3370
如何高效使用Gherkin
推荐一款基于业务行为驱动开发(BDD)测试框架:Cucumber!
Cucumber是一个行为驱动开发(BDD)工具,它结合了文本描述和自动化测试脚本。它使用一种名为Gherkin的特定语言来描述应用程序的行为,这种语言非常接近自然语言,使得非技术人员也能够理解和参与测试。
测试开发技术
2024/06/25
3670
推荐一款基于业务行为驱动开发(BDD)测试框架:Cucumber!
行为驱动开发:一篇文章带你用 Python 玩转 BDD
相信大部分的人都听说过 BDD,即:行为驱动开发,但并未涉及到它的使用方和项目实战。
AirPython
2020/06/08
3.1K1
如何在python下建立cucumber项目
Gherkin语言使用的是主要英文关键词Scenario、Given、when 、And、Then和But等,这些关键词可以转换成中文关键词,场景、假如、当、那么等。根据用户故事,需求人员或测试人员使用Gherkin语言编写好测试场景的每个步骤
顾翔
2024/09/10
1040
如何在python下建立cucumber项目
干货 | 基于 BDD 理念的 UI 自动化测试在携程度假的应用
Leo Li,携程高级软件工程师,负责度假 BDD-Test UI 自动化测试框架的研发、维护和迭代等工作。
携程技术
2020/06/24
2.7K0
干货 | 基于 BDD 理念的 UI 自动化测试在携程度假的应用
相关推荐
译文: iOS Unit Testing and UI Testing Tutorial
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验