Android下单元测试实践——测试框架简介

1794 篇文章 51 订阅
568 篇文章 1 订阅

前言

测试代码的写法可以归纳为三部分

第一部分: 准备测试数据和定义mock行为

第二部分: 调用真实的函数

第三部分: 调用验证函数进行结果的验证

Junit4

在模块的test路径下编写测试案例。在类中使用@Test注解,就可以告诉Junit这个方法是测试方式。同时使用assert*方法,可以调用Junit进行结果的验证。

@Test
public void test() {
    assertEquals("abc", getActual());
}

Junit常用注解

除了@Test注解,还有以下常见的注解可供使用

注解作用备注
@BeforeClass会在所有的方法执行前被执行,static 方法 (全局只会执行一次,而且是第一个运行)全局的含义是在一个测试类中
@AfterClass会在所有的方法执行之后进行执行,static 方法 (全局只会执行一次,而且是最后一个运行)
@Before会在每一个测试方法被运行前执行一次 可以用来清理执行环境,保证测试用例在执行前具有干净的上下文
@After会在每一个测试方法运行后被执行一次

可选使用Junit的Rule简化代码

和Junit的@Before和@After分别作用于每一个单元测试案例的开始和结束类似,@Rule注解提供了同样的能力,但有一个好处就是执行前,执行单元测试和执行后在同一个方法中,包含在同一个上下文中,这能让我们更加灵活的处理单元测试。

使用起来也比较简单:

第一步:实现TestRule接口

public class MethodNameExample implements TestRule {

    @Override

    public Statement apply(Statement base, Description description) {

        //想要在测试方法运行之前做一些事情,就在base.evaluate()之前做

        String className = description.getClassName();

        String methodName = description.getMethodName();

        base.evaluate();  //这其实就是运行测试方法

        //想要在测试方法运行之后做一些事情,就在base.evaluate()之后做

        System.out.println("Class name: "+className +", method name: "+methodName);

        return base;

    }

}

第二步:在Test类中使用。加上@Rule注解即可

@Rule
public MethodNameExample methodNameExample = new MethodNameExample();

使用Parameterized特性减少重复测试用例(Junit5自带,Junit4需额外引入依赖)

根据不同的输入,待测试函数会有不同的输出结果,那么我们就需要针对每一类的输入,编写一个测试用例,这样才能覆盖待测函数的所有逻辑分支。(写多少个测试用例能够覆盖全所有的逻辑分支可称之为待测函数的圈复杂度).

使用Junit4提供的Parameterized Tests特性,可以帮助我们减少用例编写的代码,使测试类更清晰简单,而且数据可以从CSV文件导入。以下提供一个例子 ( 官方例子见参考资料 [11] )

第一步:引入依赖

testImplementation(rootProject.ext.dependencies.jupiter)

第二步:测试类中添加注解

@RunWith(Parameterized.class)

public class BioTest {

}

第三步:可以定义实例变量,明确输入和输出。比如这里,我们定义了2个变量,一个是预期的输出结果,一个是输入的参数。

private boolean expectedResult;
private String inputTime;

第四步:定义构造函数,在构造函数中对变量赋值

public BioPayProviderTest2(boolean expectedResult, String time) {
    this.expectedResult = expectedResult;
    this.time = time;
}

第五步:定义数据集,使用注解标注,返回一个数组,数组代表的就是Junit4需要提供给构造函数进行实例化的数据集。

@Parameterized.Parameters
public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][]{
        {true, null},
        {false, System.currentTimeMillis() + ":"},
        {true, (System.currentTimeMillis() - 73 * 3600) + ":"}
    });
}

第六步:编写测试用例。对于以下的测试用例,Junit4会使用第五步的数据进行填充,执行3次。

@Test
public void test() throws Exception {
    boolean ret = needShowDialog(time, mockWalletId);
    Assert.assertEquals(expectedResult, ret);
}

Mockito

Mockito是目前使用比较广泛的Mock框架,他可以根据需要mock出虚假的对象,在测试环境中,可以用来替换掉真实的最像,达到两大目的:

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作

使用Mockito mock对象的某些方法的行为

// 使用mock函数即可模拟一个对象,这个对象的实例变量均为默认值
BioGuidePresenter presenter = Mockito.mock(BioGuidePresenter.class);

// 设定这个mock对象被调用某一方法时,应该返回的结果
Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

使用Mockito Spy对象

// 使用spy函数即可模拟一个对象,这个对象的实例变量均为默认值
BioGuidePresenter presenter = Mockito.spy(BioGuidePresenter.class);

// 设定这个mock对象被调用某一方法时,应该返回的结果
Mockito.when(presenter.checkIsEnrolled(1)).thenReturn(true);

spy()和mock()的区别在于,未指定mock方法的返回值时,默认返回null,而为指定spy方法的返回值时,默认执行目标方法的逻辑,并返回对应逻辑执行的结果。另外有一个很重要的区别在于,使用spy的情况下,虽然提供了函数的模拟实现,但Mockito框架仍然会调用真实的代码,所以如果真实代码无法在单测下运行,则使用spy模拟会导致测试失败。

使用Mockito验证结果

// 验证mock的database对象,调用setUniqueId方法时的入参是否为12
verify(database).setUniqueId(ArgumentMatchers.eq(12));

// 验证mock的database对象的getUniqueId方法是否被调用2次
verify(database, times(2)).getUniqueId();

// 验证mock的database对象的getUniqueId方法是否被调用1次
verify(database).getUniqueId();

// 也可以使用传统的Junit判断方法判断结果是否符合预期
assertEquals("foo", spy.get(0));

使用Mockito-inline模拟静态方法

Mockito版本升级之后,支持对Static Method做Hook。前提是在build.gradle中引入Mockito-inline。

org.mockito:mockito-inline:${version}

以下是实例被测代码,当我们在测试类中调用doSomethings(),很可能无法通过调整MemoryService的返回值,控制doSomthing2的入参,从而覆盖更多的逻辑分支。此时我们就需要Hook DataEngine甚至是MemoryService,获取我们想要的返回值。

public void doSomethings() {
    DataEngine.getMemoryService().saveCacheObject("key", "abc");
    ...
    String a = DataEngine.getMemoryService().getCacheObject("key");
    doSomething2(a);
}

下面给出使用mockito-inline对静态方法的处理步骤。

一.Hook静态方法。 使用Java7的try-with-resource语法,模拟触发静态方法 (DataEngine .getMemoryService 的行为。

需要注意的是:mockService可以是通过Mockito mock出来的,也可以是我们创建的一个真实的MemoryService子类,区别在于,使用Mockito mock的MemoryService我们不需要实现所有的方法,只需要mock我们测试类中可能调用到的方法。

MemoryStoreService mockService = Mockito.mock(MemoryStoreService.class);
try (MockedStatic<DataEngine> service = Mockito.mockStatic(DataEngine.class)) {
    service.when(DataEngine::getMemoryService).thenReturn(mockService);
}

二. 使用更加智能的模拟返回方法。

我们使用较多的是thenReturn()方法,但是在本案例的场景下,我们需要功能更强大的返回方法。因为:处理模拟的入参,

1. MemoryService::saveCacheObject返回值是Void,所以无法使用thenReturn()

2. 我们需要处理入参,针对每一个saveCacheObject的模拟调用,我们都需要真实的将其保存到Map中

final Map<String, Object> pools = new HashMap<>();

//当触发了mockService的saveCacheObject方法,就会回调answer(),从而将入参的Key和Value保存到Map中
Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        pools.put((String) invocation.getArgument(0), invocation.getArgument(1));
        return null;
    }
}).when(mockService).saveCacheObject(Mockito.anyString(), Mockito.any());

当我们使用doAnswer模拟了saveCacheObject,那我们很有可能需要使用同样的策略模拟getCacheObject。就像这样:

Mockito.when(mockService.containsCachedObject(Mockito.anyString()))
    .thenAnswer(invocation -> pools.containsKey(invocation.getArgument(0)));

使用Mockito测试异步代码段

假如需要测试一段异步代码,可以使用标准的异步代码测试步骤进行。举例如下:

public void update(List<Demo> demos) {
    repo.refresh(demos, () -> {
        doSomething();
    });
}

针对上述代码,测试的基本思路是:

步骤一: 模拟一个异步回调函数

//1.判断需要的回调函数的类型,创建ArgumentCaptor
ArgumentCaptor<Repo.OnRefreshListener> captor =
ArgumentCaptor.forClass(Repo.OnRefreshListener.class);

//2.主动调用相应函数,触发Mockito框架的执行流进行到回调函数,同时将captor.capture()作为入参传入。
Mockito.verify(repo).refresh(Mockito.anyList(), captor.capture());

//3.通过captor.getValue()模拟异步回调函数
Repo.OnRefreshListener mockListener = captor.getValue();

步骤二: 主动调用异步回调接口,从而使执行流进入回调函数中

//主动调用异步函数接口,使得测试执行流进入函数体
mockListener.onResult();

步骤三: 判断是否执行了doSomething()方法,或者执行结果是否符合预期的其他判断方式。

使用Mockito-inline测试静态方法的异步代码段

假如需对以下代码进行单元测试,我们就需要用到mockito-inline.可以看到,RPC请求是通过一个静态方法发出,并且通过异步回调的形式返回结果。

public void demo(String id) {
    RpcService.send(new DemoReq(id), new RpcCallback<DemoResp>() {
        @Override
        public void onFailure(BaseReq call, String msg, String procCd, String procSts, Exception e) {
            if(listener != null){
                listener.onFailure(msg, procCd, procSts);
            }
        }
        @Override
        public void onResponse(BaseReq call, WalletDetailRespMsg response) {
            if(listener != null){
                listener.onSuccess(response);
            }
        }
    });
}

具体写法关键在定义拦截后的行为,invacation保留了调用信息,根据序号获取入参,可以对入参进行判断,之后就可以主动调用回调函数。

try (MockedStatic<RpcService> rpcMock = Mockito.mockStatic(RpcService.class)) {
    //告诉mockito,遇到RpcService.send(参数任意,参数任意)的时候,拦截
    rpcMock.when(() -> RpcManager.sendFromNative(Mockito.any(), Mockito.any()))
        .then(invocation -> {
            //拦截之后,会进入到这里。
            //invocation会保留调用的信息。通过getArgument可以获取入参
            RpcCallback callback1 = invocation.getArgument(1, RpcCallback.class);
            //主动调用callback,可以指定回调入参
            callback1.onResponse(Mockito.mock(BaseReq.class),
            Mockito.mock(WalletDetailRespMsg.class));
            return null;
        });

        //主动调用被测方法
        presenter.refreshWalletDetail(testWalletId, callback);
        Mockito.verify(callback).onSuccess(Mockito.any());
}

使用Kotlin封装Mockito-inline单测公用方法

如上,我们若使用java的try-catch-resources会显得代码臃肿,于是我们可以尝试Kotlin简化。 对于try-catch-resources,kotlin中的等价写法是使用use

//mock了Apps.getApp()这个静态方法的返回结果。传入一个高阶函数,易于进行串联调用。
fun getAppMock(action: () -> Any?) {
    Mockito.mockStatic(Apps::class.java).use { appUtilsMock ->
        appUtilsMock.`when`<Void> { Apps.getApp() }.thenReturn(null)
        action()
    }
}

如果我们封装了大量的公用mock代码,那么一段测试代码就长这样:

@Test
fun reduceWithUserRejectTest() {
    val change = HceDefaultChange(true)
    getAppMock {
        isNfcDefaultPaymentMockStatic(true) {
            checkNetMockStatic(true) {
                val actual: PaymentPageState = change.reduce(PaymentPageState())
                Assert.assertTrue(actual.showWaving)
            }
        }
    }
}

是不是和写Flutter或者Compose的UI页面一样啦~

Roboletric

如果不测试Activity页面,则不建议使用Roboletric,一是因为mockito已经能够完成几乎全部的工作,并不需要用到Roboletric,二是用Roboletric影响测试执行速度。

编写可运行的Roboletric单元测试方法

// 首先需要添加RobolectricTestRunner,作为运行Roboletric的启动器

@RunWith(RobolectricTestRunner.class)

// 其次需要使用Config配置本次单元测试的基础配置。

// 1. 如果你的电脑上运行的JAVA版本不是11以上,则需要指定sdk版本为Android 9.0以下

// 2. 可以指定shadows。shadows下文会详细解析,这里可配置可不配置,取决于具体场景

// 3. qualifiers可以配置机器的尺寸,多语言环境等,可配置可不配置,取决于具体场景。例子中指定了中文环境

@Config(sdk = {Build.VERSION_CODES.O_MR1},

        shadows = {DemoShadow.class},

        qualifiers = "zh"

)
public class DemoTest {
}

使用Roboletric模拟Activity

Roboletric的一大特点就是可以模拟Android的context。 我们可以再@Before注解的方法中使用Roboletric创建一个Activity,

@Before
public void initActivity() {
    //Intent可选
    Intent faceIntent = new Intent();
    faceIntent.putExtra(DEMO, uri.toString());
    activity = Robolectric.buildActivity(VerificationBioGuideActivity.class, faceIntent)
            .create().resume().get();
}

Roboletric调用buildActivity即可模拟一个Activity,调用create可以触发onCreate回调,调用resume可以触发onResume回调,最后调动get就可以拿到这个activity对象。拿到activity的对象之后,我们就可以通过activity进行一些操作了。例如,获取View的控件,获取字符串等。

// 获取View控件
TitleBar titleBar = (TitleBar) activity.findViewById(R.id.title_bar);

// 获取字符串
activity.getString(R.string.verification_bio_pay_title_finger_success_tips)

可以使用Roboletric模拟出来的activity作为context,如果只需要用到applicaitonContext,可以使用

RuntimeEnvironment.getApplication()

Roboletric的杀手锏——Shadows

Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。

Shadows的作用就是使用自定义的方法和类替换原先业务的方法和类,原理就是使用字节码修改技术进行动态的修改。例如,业务中A.class原先要调用B.class的C()函数,我们使用Shadows,并定义一个函数签名一样的函数D()将其作用于C()函数上。当触发A.class的调用后,程序执行流程会进入D()函数中。

自定义Shadows简介

第一步: 使用@Implements类定义需要被替换的目标类。Shadows类本身也是一个普通的Java类,只不过多了@Implements注解,所以他也可以实现接口,继承等功能。

// 表明这个Shadow期望模拟ActualClass类的行为
@Implements(ActualClass.class)
@RequiresApi(api = Build.VERSION_CODES.N)
public class ActualClassShadow {

}

第二步: 与目标函数使用相同的函数签名,并且增加@Implementation注解。可以只替换目标类中的部分函数,而其他函数仍然遵循目标类的逻辑。

//表明需要模拟ActualClass中的containsKey方法
@Implementation
public boolean containsKey(String key) {
}

第三步(可选):__constructor__可以替换构造函数

public void __constructor__(int x, int y) {
    realPoint.x = x;
    realPoint.y = y;
}

第四步(可选):使用@RealObject可以获取目标对象

@RealObject private Point realPoint;

第五步:定义好Shadows之后,我们需要再测试类的Config注解中进行注册。

@Config(shadows = {ActualClassShadow.class})

使用Chat-GPT生成单元测试案例

紧跟时事!!

我们可以使用Chat-GPT快速生成测试案例,分为以下几步:

第一步:让Chat-GPT知晓该函数的意图

第二步:告诉Chat-GPT单元测试的目标

Optional:可以指定Chat-GPT使用Junit4的一些测试特性简化单测案例

序号Chat-GPTInput
1java (输入待测代码)what's the author's intentions
2The author's intention with this Java code is to provide a methodhexStringToByteArray that can convert a hexadecimal string to abyte array. The method takes a single argument data, which is thehexadecimal string to be converted. The code first checkswhether the input string is valid and has an even number ofcharacters, since each pair of characters in the string representsa single byte. If the input string is valid, the code creates a new byte arrayresult with a length equal to half the length of the input string(since each pair of characters represents one byte). It then loopsover the input string in pairs, converting each pair of charactersto a byte and storing it in the result array. If the input string is not valid, the code logs an error message andreturns null. Overall, the intention of this method is to provide a simple andreliable way to convert hexadecimal strings to byte arrays inJava.
3A good unit test suite should aim to:- Test the function's behavior for a wide range of possible inputs- Test edge cases that the author may not have foreseen- Take advantage of the features of Parameterized to make thetests easy to write and maintain- Be easy to read and understand, with clean code anddescriptive names- Be deterministic, so that the tests always pass or fail in the same wayJunit has many convenient features that make it easy to write and maintain unit tests. We'll use them to write unit tests for the function above.For this particular function, we'll want our unit tests to handle the following diverse scenarios
4Since the code provided is in Java, we'll write our unit tests inJava using the JUnit framework. Here are some example unittests we might write for the hexStringToByteArray function: @RunWith(Parameterized.class)public class DesUtilsHexStringToByteArrayTest {    @Parameterized.Parameters    public static Collection<Object[]> data() {        return Arrays.asList(new Object[][]{                // Test cases where input is valid                {"00", new byte[]{0x00}},                {"7f", new byte[]{0x7f}},                {"deadbeef", new byte[]{(byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef}},                {"0102030405060708090a0b0c0d0e0f", new byte[]{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}},                // Test cases where input is invalid                {"", null},                {"0", null},                {"1", null},                {"a", null},                {"g", null},                {"gg", null},                {"000", null},                {"001", null},                {"00g", null},                {"0x00", null},                {"deadbeef0", null}        });    }     private String input;    private byte[] expected;     public DesUtilsHexStringToByteArrayTest(String input, byte[] expected) {        this.input = input;        this.expected = expected;    }     @Test    public void testHexStringToByteArray() {        byte[] actual = hexStringToByteArray(input);        assertArrayEquals(expected, actual);    }} In this example, we're using JUnit's Parameterized runner togenerate multiple test cases from a single test method. The datamethod returns a collection of test cases, each represented as anarray of input parameters and expected output. We have included test cases for a wide range of possible inputs,including valid input strings with different lengths, as well asinvalid input strings that don't have an even number ofcharacters, contain non-hexadecimal characters, or exceed themaximum length that can be converted to a byte array. We've used descriptive names for our test class, test method,and test parameters to make the tests easy to read andunderstand. Finally, our tests are deterministic, since they will always pass or fail in the same way.

最后: 为了回馈铁杆粉丝们,我给大家整理了完整的软件测试视频学习教程,朋友们如果需要可以自行免费领取【保证100%免费】

在这里插 入图片描述

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内容简介   本书内容上涵盖了用android开发的大部分场景,从android基础介绍、环境搭建、sdk介绍、market使用,到应用剖析、组件介绍、实例演示等方面。从技术实现上,讲解了5个android平台下的完整综合实例及源代码分析,分别是rss阅读器、基于google map的个人gps、豆瓣网(web 2.0)客户端、在线音乐播放器、手机信息查看助手。本书注重对实际动手能力的指导,在遵循技术研发知识体系的严密性同时,在容易产生错误、不易理解的环节配以了翔实的开发情景截图,并将重要的知识点和开发技巧以“小实验”、“小提醒”、“小知识”、“注意”等的活泼形式呈现给读者。在程序实例的讲解方面,主要将实例安插在android开发的精髓知识章节,这为初学者学习与实践结合提供了很好的指导。.    本书配套有400多分钟的全程开发视频光盘,指导读者快速、无障碍地学通android实战开发技术。..    本书适合具备一定软件开发经验,想快速进入android开发领域的程序员;具备一些手机开发经验的开发者和android开发爱好者学习用书;也适合作为相关培训学校的android培训教材。... 目录 第1章 掀起你的盖头来——初识android. 1 1.1 认识android 1 1.2 android的背景 2 1.2.1 android的历史 2 1.2.2 android的发展 2 1.3 我的android我做主 2 1.3.1 开发基于android平台的应用 3 1.3.2 参加android开发者大赛 3 1.3.3 个人英雄主义再现——得到更多人的认可和尊重 3 1.3.4 获得应有的收益——android market 3 1.4 真实体验——android模拟器 4 1.4.1 模拟器概述 4 1.4.2 模拟器和真机的区别 4 1.4.3 模拟器使用注意事项 4 1.5 更上一层楼——加入android开发社区 5 1.6 本章小结 6 第2章 工欲善其事 必先利其器——搭建android开发环境 7 2.1 开发android应用前的准备 7 2.1.1 android开发系统要求 7 2.1.2 android软件开发包 7 .2.1.3 其他注意事项 8 2.2 windows开发环境搭建 8 2.2.1 jdk、eclipse、android sdk软件安装 8 2.2.2 sdk的家在哪里——设定android sdk home 14 2.2.3 真的准备好了吗——开发环境验证 14 2.2.4 创建android 虚拟设备(avd) 15 2.3 linux一族——ubuntu开发环境搭建 17 2.3.1 java、eclipse和adt插件安装 17 2.3.2 设定android sdk home 23 2.4 mac os一族——苹果开发环境搭建 24 2.5 本章小结 24 第3章 清点可用资本——android sdk介绍 25 3.1 android sdk 基础 25 3.2 深入探寻android sdk的密码 25 3.2.1 android sdk目录结构 25 3.2.2 android.jar及内部结构 27 3.2.3 sdk文档及阅读技巧 27 3.2.4 先来热热身——android sdk例子解析 28 3.2.5 sdk提供的工具介绍 31 3.3 android典型包分析 33 3.3.1 开发的基石——android api核心开发包介绍 33 3.3.2 拓展开发外延——android可选api介绍 34 3.4 本章小结 34 第4章 赚钱的市场——android market及应用发布 35 4.1 google market产生背景与目的 35 4.2 体验“选货”的乐趣——在g1上体验market的使用 35 4.3 android开发活动及特色应用 37 4.3.1 开发应用的领域 37 4.3.2 android market特色应用一览 38 4.4 你也可以做东家——申请market账号 43 4.4.1 卖东西要先入伙——准备工作 43 4.4.2 入伙过程——申请 44 4.5 开张了——在market上发布应用 45 4.5.1 发布时可能遇到的错误 45 4.5.2 卖东西也要签名——生成签名文件 46 4.5.3 打包、签名、发布应用 48 4.6 本章小结 51 第5章 千里之行 始于足下——第一个应用helloworld 52 5.1 helloworld应用分析 52 5.1.1 新建一个andr
['ANDROID APP安全从入门到放弃.pdf', 'CNCERT车联网众测平台与漏洞库介绍.pdf', 'IOS URL Scheme之殇.pdf', 'OT&核心基础架构安全.pdf', 'RedTeam视角下的二进制攻防研究.pdf', 'Reflection on trusting trust.pdf', 'Web漏洞挖掘速成特训营.pdf', 'XAI与可信任安全智.pdf', '云安全自动化-Fortinet.pdf', '云租户等保合规探索.pdf', '代码能力在渗透测试实战中的价值.pdf', '以子之矛攻子之盾,AI攻防的那些事.pdf', '你相信发电厂爆炸事件是工控黑客所为吗?.pdf', '加密流量恶意软件分析在金融场景的实践.pdf', '华泰证券在金融安全风险运营的实践.pdf', '卫星通信的安全缺陷.pdf', '域权限维持方法浅析.pdf', '基于图数据的云上BOT团伙深度感知.pdf', '威胁可感知_安全可运维.pdf', '安信证券安全运营实践分享.pdf', '安全众测下的漏洞发展新趋势.pdf', '安全建设从一人到一众.pdf', '安全运营之能力升级.pdf', '工业互联网软件代码安全技术发展及趋势.pdf', '打造CTF+X透测试X奋混合剂-存在敏感词.pdf', '技术供应商的合规与安全.pdf', '提升金融机构网络安全成熟度.pdf', '揭开神“密”面纱 密码服务进万家.pdf', '支撑等保2.0.pdf', '新形势下的企业安全体系建设实.pdf', '汽车信息安全中的后门入口.pdf', '消费类物联网IoT的安全实现.pdf', '猫鼠游戏 持续渗透中的高级命令混淆对.pdf', '现代“碟中谍”——多国APT组织的杀戮之路.pdf', '硬件盒子安全分解.pdf', '端到端物联网通信安全技术框架.pdf', '给我1K内存VS难以打破的安全系.pdf', '网络安全等级保护2.0标准解读.pdf', '网络攻防演练的现实思考.pdf', '腾讯云数据安全中台.pdf', '蓝方的进攻-进攻是最好的防守.pdf', '超越合规视角的安全治理框架.pdf', '部分可观下APT攻击行为捕获:马尔可夫决策助力AI模型.pdf', '金融科技应用安全风险监测实践分享.pdf', '金融行业安全业务系统的优化与编排.pdf', '针对云端AI服务的攻击和防护.pdf', '锤炼新形势下实网攻防的“剑与盾”.pdf']

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值