先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024b (备注软件测试)
正文
public class IsAllLowerCase implements ValidationStrategy {
public boolean execute(String s){
return s.matches(“[a-z]+”);
}
}
public class IsNumeric implements ValidationStrategy {
public boolean execute(String s){
return s.matches(“\d+”);
}
}
public class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s);
}
}
使用Lambda表达式
到现在为止,你应该已经意识到ValidationStrategy是一个函数接口了(除此之外,它还与Predicate具有同样的函数描述)。这意味着我们不需要声明新的类来实现不同的策略,通过直接传递Lambda表达式就能达到同样的目的,并且还更简洁:
Validator numericValidator = new Validator((String s) -> s.matches(“[a-z]+”));
boolean b1 = numericValidator.validate(“aaaa”);
Validator lowerCaseValidator = new Validator((String s) -> s.matches(“\d+”));
boolean b2 = lowerCaseValidator.validate(“bbbb”);
Lambda表达式避免了采用策略设计模式时僵化的模板代码。
模板方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。换句话说,模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时是非常有用的。
假设你需要编写一个简单的在线银行应用。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到用户的详细信息,最终完成一些让用户满意的操作。
不同分行的在线银行应用让客户满意的方式可能还略有不同,比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来实现在线银行应用:
abstract class OnlineBanking {
public void processCustomer(int id){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy©;
}
abstract void makeCustomerHappy(Customer c);
}
使用Lambda表达式2
public void processCustomer(int id, Consumer makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept©;
}
new OnlineBankingLambda()
.processCustomer(1337, (Customer c) -> System.out.println("Hello " + c.getName());
Lamba表达式能帮助你解决设计模式与生俱来的设计僵化问题
观察者模式
观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。
创建图形用户界面(GUI)程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件(比如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。
但是观察者模式并不局限于图形用户界面。比如,观察者设计模式也适用于股票交易的情形,多个券商可能都希望对某一支股票价格(主题)的变动做出响应。
你需要为Twitter这样的应用设计并实现一个定制化的通知系统。想法很简单:好几家报纸机构,比如《纽约时报》《卫报》以及《世界报》都订阅了新闻,他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。
观察者
interface Observer {
void notify(String tweet);
}
class NYTimes implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains(“money”)){
System.out.println("Breaking news in NY! " + tweet);
}
}
}
class Guardian implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains(“queen”)){
System.out.println("Yet another news in London… " + tweet);
}
}
}
class LeMonde implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains(“wine”)){
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
主题
interface Subject{
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
class Feed implements Subject{
private final List observers = new ArrayList<>();
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers(“The queen said her favourite book is Java 8 in Action!”);
使用Lambda表达式3
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains(“money”)){
System.out.println("Breaking news in NY! " + tweet);
}
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains(“queen”)){
System.out.println("Yet another news in London… " + tweet);
}
});
是否我们随时随地都可以使用Lambda表达式呢?答案是否定的!
Lambda适配得很好,那是因为需要执行的动作都很简单,因此才能很方便地消除僵化代码。但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此类。在这些情形下,你还是应该继续使用类的方式。
责任链模式
责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。
通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。
public abstract class ProcessingObject {
protected ProcessingObject successor;
public void setSuccessor(ProcessingObject successor){
this.successor = successor;
public T handle(T input){
T r = handleWork(input);
if(successor != null){
return successor.handle®;
}
return r;
}
abstract protected T handleWork(T input);
}
}
可以创建两个处理对象,它们的功能是进行一些文
本处理工作。
public class HeaderTextProcessing extends ProcessingObject {
public String handleWork(String text){
return "From Raoul, Mario and Alan: " + text;
}
}
public class SpellCheckerProcessing extends ProcessingObject {
public String handleWork(String text){
return text.replaceAll(“labda”, “lambda”);
}
}
现在你就可以将这两个处理对象结合起来,构造一个操作序列!
ProcessingObject p1 = new HeaderTextProcessing();
ProcessingObject p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle(“Aren’t labdas really sexy?!!”);
System.out.println(result);
使用Lambda表达式4
这个模式看起来像是在链接(也即是构造)函数,你需要使用andThen方法对其进行构造。
UnaryOperator headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator spellCheckerProcessing = (String text) -> text.replaceAll(“labda”, “lambda”);
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = pipeline.apply(“Aren’t labdas really sexy?!!”);
工厂模式
使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,我们假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。
public class ProductFactory {
public static Product createProduct(String name){
switch(name){
case “loan”: return new Loan();
case “stock”: return new Stock();
case “bond”: return new Bond();
default: throw new RuntimeException("No such product " + name);
}
}
}
这里贷款(Loan)、股票(Stock)和债券(Bond)都是产品(Product)的子类。createProduct方法可以通过附加的逻辑来设置每个创建的产品。但是带来的好处也显而易见,你在创建对象时不用再担心会将构造函数或者配置暴露给客户,这使得客户创建产品时更加简单:
Product p = ProductFactory.createProduct(“loan”);
使用Lambda表达式5
Supplier loanSupplier = Loan::new;
Loan loan = loanSupplier.get();
final static Map<String, Supplier> map = new HashMap<>();
static {
map.put(“loan”, Loan::new);
map.put(“stock”, Stock::new);
map.put(“bond”, Bond::new);
}
现在,你可以像之前使用工厂设计模式那样,利用这个Map来实例化不同的产品。
public static Product createProduct(String name){
Supplier p = map.get(name);
if(p != null)
return p.get();
throw new IllegalArgumentException("No such product " + name);
}
这是个全新的尝试,它使用Java 8中的新特性达到了传统工厂模式同样的效果。但是,如果工厂方法createProduct需要接收多个传递给产品构造方法的参数,这种方式的扩展性不是很好。你不得不提供不同的函数接口,无法采用之前统一使用一个简单接口的方式。
比如,我们假设你希望保存具有三个参数(两个参数为Integer类型,一个参数为String类型)的构造函数;为了完成这个任务,你需要创建一个特殊的函数接口TriFunction。最终的结果是Map变得更加复杂。
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
Map<String, TriFunction<Integer, Integer, String, Product>> map = new HashMap<>();
测试可见Lambda 函数的行为
由于moveRightBy方法声明为public,测试工作变得相对容易。你可以在用例内部完成测试。但是Lambda并无函数名(毕竟它们都是匿名函数),因此要对你代码中的Lambda函数进行测试实际上比较困难,因为你无法通过函数名的方式调用它们。
有些时候,你可以借助某个字段访问Lambda函数,这种情况,你可以利用这些字段,通过它们对封装在Lambda函数内的逻辑进行测试。比如,我们假设你在Point类中添加了静态字段compareByXAndThenY,通过该字段,使用方法引用你可以访问Comparator对象:
public class Point{
public final static Comparator compareByXAndThenY = comparing(Point::getX)
.thenComparing(Point::getY);
…
}
Lambda表达式会生成函数接口的一个实例。由此,你可以测试该实例的行为。这个例子中,我们可以使用不同的参数,对Comparator对象类型实例compareByXAndThenY的compare方法进行调用,验证它们的行为是否符合预期:
@Test
public void testComparingTwoPoints() throws Exception {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 20);
int result = Point.compareByXAndThenY.compare(p1 , p2);
assertEquals(-1, result);
}
测试使用Lambda 的方法的行为
我们需要对使用Lambda表达式的方法进行测试。比如下面这个方法moveAllPointsRightBy:
public static List moveAllPointsRightBy(List points, int x){
return points.stream()
.map(p -> new Point(p.getX() + x, p.getY()))
.collect(toList());
}
没必要对Lambda表达式p -> new Point(p.getX() + x,p.getY())进行测试,它只是moveAllPointsRightBy内部的实现细节。我们更应该关注的是方法moveAllPointsRightBy的行为:
@Test
public void testMoveAllPointsRightBy() throws Exception{
List points =
Arrays.asList(new Point(5, 5), new Point(10, 5));
List expectedPoints =
Arrays.asList(new Point(15, 5), new Point(20, 5));
List newPoints = Point.moveAllPointsRightBy(points, 10);
assertEquals(expectedPoints, newPoints);
}
将复杂的Lambda 表达式分到不同的方法
可能你会碰到非常复杂的Lambda表达式,包含大量的业务逻辑,比如需要处理复杂情况的定价算法。你无法在测试程序中引用Lambda表达式如果一个方法接受Lambda表达式作为参数,你可以采用的一个方案是使用不同的Lambda表达式对它进行测试。
调试有问题的代码时,程序员的兵器库里有两大老式武器,分别是:
-
查看栈跟踪
-
输出日志
查看栈跟踪
你的程序突然停止运行(比如突然抛出一个异常),这时你首先要调查程序在什么地方发生了异常以及为什么会发生该异常。这时栈帧就非常有用。程序的每次方法调用都会产生相应的调用信息,包括程序中方法调用的位置、该方法调用使用的参数、被调用方法的本地变量。这些信息被保存在栈帧上。
程序失败时,你会得到它的栈跟踪,通过一个又一个栈帧,你可以了解程序失败时的概略信息。换句话说,通过这些你能得到程序失败时的方法调用列表。这些方法调用列表最终会帮助你发现问题出现的原因。
不幸的是,由于Lambda表达式没有名字,它的栈跟踪可能很难分析。在下面这段简单的代码中,我们刻意地引入了一些错误:
import java.util.*;
public class Debugging{
public static void main(String[] args) {
List points = Arrays.asList(new Point(12, 2), null);
points.stream().map(p -> p.getX()).forEach(System.out::println);
}
}
运行这段代码会产生下面的栈跟踪:
Exception in thread “main” java.lang.NullPointerException
at Debugging.lambda$main$0(Debugging.java:6)
at Debugging$$Lambda$5/284720968.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline
.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators
.java:948)
…
这段程序当然会失败,因为Points列表的第二个元素是空(null)。这时你的程序实际是在试图处理一个空引用。由于Stream流水线发生了错误,构成Stream流水线的整个方法调用序列都暴露在你面前了。
不过,你留意到了吗?栈跟踪中还包含下面这样类似加密的内容:
at Debugging.lambda$main$0(Debugging.java:6)
at Debugging$$Lambda$5/284720968.apply(Unknown Source)
这些表示错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。这个例子中,它的名字是lambda$main$0,看起来非常不直观。如果你使用了大量的类,其中又包含多个Lambda表达式,这就成了一个非常头痛的问题。
即使你使用了方法引用,还是有可能出现栈无法显示你使用的方法名的情况。将之前的Lambda表达式p-> p.getX()替换为方法引用reference Point::getX也会产生难于分析的栈跟踪:
points.stream().map(Point::getX).forEach(System.out::println);
Exception in thread “main” java.lang.NullPointerException
at Debugging$$Lambda$5/284720968.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline
.java:193)
…
注意,如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。比如,下面这个例子:
import java.util.*;
public class Debugging{
public static void main(String[] args) {
List numbers = Arrays.asList(1, 2, 3);
numbers.stream().map(Debugging::divideByZero).forEach(System.out::println);
}
public static int divideByZero(int n){
return n / 0;
}
}
方法divideByZero在栈跟踪中就正确地显示了:
Exception in thread “main” java.lang.ArithmeticException: / by zero
at Debugging.divideByZero(Debugging.java:10)
at Debugging$$Lambda$1/999966131.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline
.java:193)
…
总的来说,我们需要特别注意,涉及Lambda表达式的栈跟踪可能非常难理解。这是Java编译器未来版本可以改进的一个方面。
使用日志调试
使用forEach将流操作的结果日志输出到屏幕上或者记录到日志文件中:
List numbers = Arrays.asList(2, 3, 4, 5);
numbers.stream()
.map(x -> x + 17)
.filter(x -> x % 2 == 0)
.limit(3)
.forEach(System.out::println);
这段代码的输出如下:
20
22
不幸的是,一旦调用forEach,整个流就会恢复运行。到底哪种方式能更有效地帮助我们理解Stream流水线中的每个操作(比如map、filter、limit)产生的输出?
peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。
List result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
元素恢复运行之前,插入执行一个动作。但是它不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。
List result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-trm7l81u-1713431213378)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!