如何让自动化测试更加灵活简洁?

简化的架构对于自动化测试和主代码一样重要。冗余和不灵活性可能会导致一些问题:比如 UI 中的任何更改都需要更新多个文件,测试可能在功能上相互重复,并且支持新功能可能会变成一项耗时且有挑战性的工作来适应现有测试。

页面对象模式如何理顺代码

在为应用程序编写测试时,我们需要在运行各种检查或操作时引用应用程序的视图元素。如果我们总是在编写的每个测试中明确说明元素 ID,这将使我们的代码容易受到 UI 更改的影响:我们必须在使用这些元素的每个测试中更新所有已更改的 ID。

页面对象模式有助于避免这种情况。页面对象模式的理念是将页面(应用程序屏幕)作为一个对象(测试抽象)来呈现,该对象会公布和初始化页面上的所有图形元素,并设置与它们的交互。有关该模式的详细信息可以在此处了解(https://kasperskylab.github.io/Kaspresso/en/Wiki/Page_object_in_Kaspresso/)。

本文中的所有示例都使用我们的开源测试自动化框架 Kaspresso。(https://github.com/KasperskyLab/Kaspresso)为什么不使用Espresso?

首先,Kaspresso使用声明式方法编写测试,这种方法依赖于Kakao,它是Espresso的Kotlin DSL封装器。下面是一个例子:

Espresso

 
  1. @Test

  2. fun testFirstFeature() {

  3. onView(withId(R.id.toFirstFeature))

  4. .check(ViewAssertions.matches(

  5. ViewMatchers.withEffectiveVisibility(

  6. ViewMatchers.Visibility.VISIBLE)))

  7. onView(withId(R.id.toFirstFeature)).perform(click())

  8. }

Kaspresso

 
  1. @Test

  2. fun testFirstFeature() {

  3. MainScreen {

  4. toFirstFeatureButton {

  5. isVisible()

  6. click()

  7. }

  8. }

  9. }

其次,在拦截器的帮助下,Kaspresso 避免了测试的不稳定性,从而提高了稳定性。这些拦截器在我们处理异步图形元素或列表时特别有用。

第三,Kaspresso集成了KAutomator,这是一个方便的Kotlin DSL封装器,可用于UI Automator,从而加快UI测试的速度。下面是标准版(右)和加速版(左)UI Automator之间的区别:

图片

除此之外,Kaspresso 允许将测试分解为步骤,类似于手动测试用例的完成方式,并记录每个步骤。如果测试崩溃,日志将帮助你立即查看哪些步骤成功完成,哪些步骤失败。除了日志之外,你还可以访问图形元素的层次结构以及视频、屏幕截图等。Kaspresso 内置的 Android 调试桥 (adb) 支持将帮助你直接使用 Android。Allure集成可清晰显示测试结果。

那么,让我们开始讨论正题。你可以通过下载项目源代码并运行它来重现下面描述的所有步骤。我们将描述 MainActivity 页面并自动化 LoginActivity 测试。结果以及测试可在TECH-tutorial-results分支中找到,因此你可以随时前往那里查看完成的代码。

MainActivity 看起来像这样:

图片

我们创建一个继承自 KScreen 的 MainScreen 对象:

 
  1. object MainScreen : KScreen<MainScreen>() {

  2. override val layoutId: Int? = null

  3. override val viewClass: Class<*>? = null

  4. }

KScreen 实现了页面对象模式,它描述了与测试交互的所有视图元素。

Kaspresso 中的页面对象实现以 layoutId 和 viewClass 变量而闻名,它们可以帮助开发人员立即识别哪个布局文件用于相关页面以及哪个类提供其功能。但手头的任务是讨论页面对象概念本身,因此我们现在将它们设置为 null。

我们使用 Android Studio 中的 UI Automator Viewer 或 Layout Inspector 来查找登录活动按钮的 ID。页面上其余视图元素的标识符可以类似地找到。

图片

主屏幕元素的描述如下所示:

 
  1. object MainScreen : KScreen<MainScreen>() {

  2. override val layoutId: Int? = null

  3. override val viewClass: Class<*>? = null

  4. val titleTextView = KTextView { withId(R.id.title) }

  5. val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }

  6. val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }

  7. val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }

  8. val loginActivityButton = KButton { withId(R.id.login_activity_btn) }

  9. val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }

  10. val flakyActivityButton = KButton { withId(R.id.flaky_activity_btn) }

  11. val listActivityButton = KButton { withId(R.id.list_activity_btn) }

  12. }

现在,我们可以从我们创建的任何测试中引用 MainScreen 对象,并使用此页面的视图元素。

让我们编写第一个测试,它将检查页面上是否有“登录活动”按钮并单击它。

为此,我们创建一个继承自 TestCase 的 LoginActivityTest 类:

 
  1. class LoginActivityTest : TestCase() {

  2. /**

  3. * activityScenarioRule is used to invoke MainActivity before running the test.

  4. * More details on activityScenarioRule are available here:

  5. * https://developer.android.com/reference/androidx/test/ext/junit/rules/ActivityScenarioRule

  6. */

  7. @get:Rule

  8. val activityRule = activityScenarioRule<MainActivity>()

  9. @Test

  10. fun test() {

  11. MainScreen {

  12. loginActivityButton {

  13. isVisible()

  14. click()

  15. }

  16. }

  17. }

  18. }

图片

...并创建登录屏幕:

 
  1. object LoginScreen : KScreen<LoginScreen>() {

  2. override val layoutId: Int? = null

  3. override val viewClass: Class<*>? = null

  4. val usernameEditText = KEditText { withId(R.id.input_username) }

  5. val passwordEditText = KEditText { withId(R.id.input_password) }

  6. val loginButton = KButton { withId(R.id.login_btn) }

  7. }

让我们修改 LoginActivityTest 并尝试使用登录名“123456”和密码“123456”获得授权:

 
  1. class LoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. val username = "123456"

  7. val password = "123456"

  8. MainScreen {

  9. loginActivityButton {

  10. isVisible()

  11. click()

  12. }

  13. }

  14. LoginScreen {

  15. usernameEditText { replaceText(username) }

  16. passwordEditText { replaceText(password) }

  17. loginButton { click() }

  18. }

  19. }

  20. }

授权后,我们会看到最后一个页面 AfterLoginActivity。

图片

Kaspresso 可以使用Device类从测试内部检查正在显示的活动。我们通过检查授权后设备屏幕上是否出现 AfterLoginActivity 来结束第一个测试:

 
  1. class LoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. val username = "123456"

  7. val password = "123456"

  8. MainScreen {

  9. loginActivityButton {

  10. isVisible()

  11. click()

  12. }

  13. }

  14. LoginScreen {

  15. usernameEditText { replaceText(username) }

  16. passwordEditText { replaceText(password) }

  17. loginButton { click() }

  18. }

  19. device.activities.isCurrent(AfterLoginActivity::class.java)

  20. }

  21. }

这种方法使得动态了解哪些测试字符串与哪些页面交互变得更加困难。添加新的检查和操作可能会使代码难以辨认。因此,我们建议使用页面对象来创建高质量的可扩展测试。

将测试分为几个步骤

任何测试,无论是自动测试还是手动测试,都要遵循一个测试用例--也就是说,测试人员要检查一连串的步骤,以确定页面是否功能齐全。在 step() 函数的帮助下,Kaspresso 将代码分解成多个步骤。步骤还有助于整理测试日志。

要使用步骤,需要在测试中调用 run{} 方法,并在大括号中列出测试中要运行的所有步骤。每个步骤都应在 step() 函数中调用。

让我们试一下:​​​​​​​

 
  1. class LoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. run {

  7. val username = "123456"

  8. val password = "123456"

  9. step("Open login screen") {

  10. MainScreen {

  11. loginActivityButton {

  12. isVisible()

  13. click()

  14. }

  15. }

  16. }

  17. step("Try to login") {

  18. LoginScreen {

  19. usernameEditText { replaceText(username) }

  20. passwordEditText { replaceText(password) }

  21. loginButton { click() }

  22. }

  23. }

  24. step("Check current screen") {

  25. device.activities.isCurrent(AfterLoginActivity::class.java)

  26. }

  27. }

  28. }

  29. }

通过这些步骤,标记为“KASPRESSO”的信息级日志如下所示:

图片

如果你对步骤仍有疑问,建议你阅读这些(https://kasperskylab.github.io/Kaspresso/en/Tutorial/Steps_and_sections/)。它还提供了你可能在日志中注意到的之前/之后部分的详细信息。

现在,让我们尝试实施负面测试用例,例如用户输入的登录名或密码少于最小字符数(6 个)。

在创建一组自动测试时,应遵循的规则是为每个测试用例设置一个单独的测试方法。换句话说,我们不会在同一个方法中测试输入无效登录名或密码时的行为,而是在 LoginActivityTest 类中创建单独的方法:​​​​​​​

 
  1. @Test

  2. fun loginUnsuccessfulIfUsernameIncorrect() {

  3. run {

  4. val username = "12"

  5. val password = "123456"

  6. step("Open login screen") {

  7. MainScreen {

  8. loginActivityButton {

  9. isVisible()

  10. click()

  11. }

  12. }

  13. }

  14. step("Try to login") {

  15. LoginScreen {

  16. usernameEditText { replaceText(username) }

  17. passwordEditText { replaceText(password) }

  18. loginButton { click() }

  19. }

  20. }

  21. step("Check current screen") {

  22. device.activities.isCurrent(LoginActivity::class.java)

  23. }

  24. }

  25. }

另一个测试,使用有效的登录名和无效的密码:​​​​​​​

 
  1. @Test

  2. fun loginUnsuccessfulIfPasswordIncorrect() {

  3. run {

  4. val username = "123456"

  5. val password = "1234"

  6. step("Open login screen") {

  7. MainScreen {

  8. loginActivityButton {

  9. isVisible()

  10. click()

  11. }

  12. }

  13. }

  14. step("Try to login") {

  15. LoginScreen {

  16. usernameEditText { replaceText(username) }

  17. passwordEditText { replaceText(password) }

  18. loginButton { click() }

  19. }

  20. }

  21. step("Check current screen") {

  22. device.activities.isCurrent(LoginActivity::class.java)

  23. }

  24. }

  25. }

我建议你在执行第一个测试时重命名它,以便其名称显示我们仅检查是否成功授权。​​​​​​​

 
  1. @Test

  2. fun test()

我们将其更改为:​​​​​​​

 
  1. @Test

  2. fun loginSuccessfulIfUsernameAndPasswordCorrect()

你可能已经注意到,在上面的自动化测试中,用于导航到 LoginActivity 页面并输入登录凭据的字符串会重复。如果能重复使用这些步骤就好了。

使用Scenario

Kaspresso 包含一个名为 Scenario 的工具,它允许将多个步骤组合成有序的操作序列。这在编写重复步骤的测试时非常有用。

让我们创建一个继承自 Scenario 的 LoginScenario 类。为了使其工作,我们需要重写 steps 属性以列出Scenario中的所有步骤。​​​​​​​

 
  1. class LoginScenario : Scenario() {

  2. override val steps: TestContext<Unit>.() -> Unit = {

  3. step("Open login screen") {

  4. MainScreen {

  5. loginActivityButton {

  6. isVisible()

  7. click()

  8. }

  9. }

  10. }

  11. step("Try to login") {

  12. LoginScreen {

  13. usernameEditText { replaceText(username) }

  14. passwordEditText { replaceText(password) }

  15. loginButton { click() }

  16. }

  17. }

  18. }

  19. }

class LoginScenario : Scenario()

更改为:​​​​​​​

 
  1. class LoginScenario(

  2. private val username: String,

  3. private val password: String

  4. ) : Scenario()

这是生成的Scenario代码:​​​​​​​

 
  1. class LoginScenario(

  2. private val username: String,

  3. private val password: String

  4. ) : Scenario() {

  5. override val steps: TestContext<Unit>.() -> Unit = {

  6. step("Open login screen") {

  7. MainScreen {

  8. loginActivityButton {

  9. isVisible()

  10. click()

  11. }

  12. }

  13. }

  14. step("Try to login") {

  15. LoginScreen {

  16. usernameEditText { replaceText(username) }

  17. passwordEditText { replaceText(password) }

  18. loginButton { click() }

  19. }

  20. }

  21. }

  22. }​​​​​​​

让我们在 LoginActivityTest 测试中使用此Scenario:​​​​​​​

 
  1. class LoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun loginSuccessfulIfUsernameAndPasswordCorrect() {

  6. run {

  7. step("Try to login with correct username and password") {

  8. scenario(

  9. LoginScenario(

  10. username = "123456",

  11. password = "123456",

  12. )

  13. )

  14. }

  15. step("Check current screen") {

  16. device.activities.isCurrent(AfterLoginActivity::class.java)

  17. }

  18. }

  19. }

  20. @Test

  21. fun loginUnsuccessfulIfUsernameIncorrect() {

  22. run {

  23. step("Try to login with incorrect username") {

  24. scenario(

  25. LoginScenario(

  26. username = "12",

  27. password = "123456",

  28. )

  29. )

  30. }

  31. step("Check current screen") {

  32. device.activities.isCurrent(LoginActivity::class.java)

  33. }

  34. }

  35. }

  36. @Test

  37. fun loginUnsuccessfulIfPasswordIncorrect() {

  38. run {

  39. step("Try to login with incorrect password") {

  40. scenario(

  41. LoginScenario(

  42. username = "123456",

  43. password = "1234",

  44. )

  45. )

  46. }

  47. step("Check current screen") {

  48. device.activities.isCurrent(LoginActivity::class.java)

  49. }

  50. }

  51. }

  52. }​​​​​​​

我们研究了一种有利于使用Scenario的案例——在同一页面的不同测试中重复使用相同的步骤。然而,这并不是Scenario的唯一目的。

一个应用程序可以有多个页面,你只能以授权用户的身份访问这些页面。然后,你需要重新描述每个页面的授权步骤。但是,如果你使用Scenario,这将变得非常简单。

目前,AfterLoginActivity 页面在我们登录后打开。让我们为该屏幕编写一个测试。

首先我们创建一个页面对象:​​​​​​​

 
  1. object AfterLoginScreen : KScreen<AfterLoginScreen>() {

  2. override val layoutId: Int? = null

  3. override val viewClass: Class<*>? = null

  4. val title = KTextView { withId(R.id.title) }

  5. }

然后我们添加测试:​​​​​​​

 
  1. class AfterLoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. }

  7. }

我们需要获得授权才能访问该页面。如果没有Scenario,我们将不得不再次重新运行所有步骤:打开主页,单击按钮,输入登录名和密码,然后再次单击按钮。整个过程现在简化为使用 LoginScenario:​​​​​​​

 
  1. class AfterLoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. run {

  7. step("Open AfterLogin screen") {

  8. scenario(

  9. LoginScenario(

  10. username = "123456",

  11. password = "123456"

  12. )

  13. )

  14. }

  15. step("Check title") {

  16. AfterLoginScreen {

  17. title {

  18. isVisible()

  19. }

  20. }

  21. }

  22. }

  23. }

  24. }

总而言之,使用Scenario使代码干净、清晰且可重用。如果你想要测试仅授权用户可以访问的页面,则无需再重复大量相同的步骤。重要的是,我们还实现了适当的测试可扩展性。如果 LoginActivity 页面上的 UI 元素的标识符发生更改,则不需要更新测试代码。要使测试再次正常工作,你所需要做的就是修复 LoginScreen。

作为对比,这里是没有以上最佳实践的测试代码。我希望你能像一场噩梦一样忘记这种写作风格。​​​​​​​

 
  1. class LoginActivityTest : TestCase() {

  2. @get:Rule

  3. val activityRule = activityScenarioRule<MainActivity>()

  4. @Test

  5. fun test() {

  6. val loginActivityButton = KButton { withId(R.id.login_activity_btn) }

  7. loginActivityButton {

  8. isVisible()

  9. click()

  10. }

  11. val usernameEditText = KEditText { withId(R.id.input_username) }

  12. val passwordEditText = KEditText { withId(R.id.input_password) }

  13. val loginButton = KButton { withId(R.id.login_btn) }

  14. usernameEditText { replaceText("123456") }

  15. passwordEditText { replaceText("123456") }

  16. loginButton { click() }

  17. device.activities.isCurrent(AfterLoginActivity::class.java)

  18. pressBack()

  19. usernameEditText { replaceText("123456") }

  20. passwordEditText { replaceText("1234") }

  21. loginButton { click() }

  22. device.activities.isCurrent(LoginActivity::class.java)

  23. usernameEditText { replaceText("12") }

  24. passwordEditText { replaceText("123456") }

  25. loginButton { click() }

  26. device.activities.isCurrent(LoginActivity::class.java)

  27. }

  28. }​​​​​​​

Kaspresso 框架相关链接:

https://github.com/KasperskyLab/Kaspresso

https://kasperskylab.github.io/Kaspresso/en

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/41299.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DataWhale-吃瓜教程学习笔记 (七)

学习视频**&#xff1a;第6章-支持向量机_哔哩哔哩_bilibili 西瓜书对应章节&#xff1a; 第六章 支持向量机 - 算法原理 几何角度 对于线性可分数据集&#xff0c;找距离正负样本距离都最远的超平面&#xff0c;解是唯一的&#xff0c;泛化性能较好 - 超平面 - 几何间隔 例…

MSPM0G3507——读取引脚的高低电平方法(数字信号循迹模块)

SYSCFG配置 代码部分 //第一个传感器if( DL_GPIO_readPins(xunji_PORT_PIN1_PORT , xunji_PORT_PIN1_PIN )xunji_PORT_PIN1_PIN) //黑&#xff0c;不亮 高{a1;}if( DL_GPIO_readPins(xunji_PORT_PIN1_PORT , xunji_PORT…

从 Keycloak 导出和导入 Realm 和用户

1. 首先对keycloak 命令有所了解 需要将 Keycloak 中的 Realm 导出或导入时&#xff0c;您可以使用 JSON 文件进行操作。以下是一些有关导出和导入 Realm 的方法&#xff1a; 导出 Realm 到目录&#xff1a; 使用 export 命令将 Realm 导出到目录。在执行此命令时&#xff0c;…

技术分享:直播平台如何开发并接入美颜SDK

本篇文章&#xff0c;笔者将分享直播平台如何开发并接入美颜SDK的技术细节与步骤。 一、选择合适的美颜SDK 首先&#xff0c;选择一款适合的美颜SDK非常重要。市面上有很多优秀的美颜SDK供应商&#xff0c;选择时应考虑以下因素&#xff1a; 功能丰富性&#xff1a;支持美白…

短视频文案提取神器怎么提取抖音视频文案!

很多编导以及视频内容创作者为了提高自己的工作效率还会使用视频转文字提取神器&#xff0c;我们都清楚短视频领域每个平台人群熟悉都有所不同&#xff0c;在分发内容的时候也会调整内容已符合平台属性。 短视频文案提取神器怎么提取抖音视频文案 短视频常见的平台有抖音、西瓜…

linux ifconfig未找到命令

linux ifconfig未找到命令 1、使用yum安装net-tools yum install net-toolsyum报未找到命令请查看文章vim未找到命令&#xff0c;且yum install vim安装vim失败 2、安装后使用ifconfig命令 ifconfig

windows实现Grafana+Loki+loki4j轻量级日志系统,告别沉重的ELK

文章目录 Loki下载Loki下载安装Loki添加Loki数据源springboot日志推送 Loki下载 下载地址&#xff1a;https://github.com/grafana/loki/releases/ 找到loki-windows-amd64.exe.zip点击开始下载&#xff0c;我这里下载的2.9.9版本 Loki下载 下载地址&#xff1a;https://gr…

springboot苏桦旅游管理系统-计算机毕业设计源码02123

摘要 旅游业在全球范围内不断发展&#xff0c;为了提供高效的旅游管理和服务&#xff0c;开发一个旅游管理系统具有重要意义。本文旨在设计和实现该旅游管理系统&#xff0c;以满足用户和管理员的需求。该系统采用Spring Boot作为后端框架&#xff0c;利用其简化的开发流程和强…

通过百度文心智能体创建STM32编程助手-实操

一、前言 文心智能体平台AgentBuilder 是百度推出的基于文心大模型的智能体&#xff08;Agent&#xff09;平台&#xff0c;支持广大开发者根据自身行业领域、应用场景&#xff0c;选取不同类型的开发方式&#xff0c;打造大模型时代的产品能力。开发者可以通过 prompt 编排的…

【并发编程JUC】AQS详解

定义理解 AQS&#xff0c;全称为AbstractQueuedSynchronizer&#xff0c;是Java并发包&#xff08;java.util.concurrent&#xff09;中的一个框架级别的工具类&#xff0c;用于构建锁和同步器。它是许多同步类的基础&#xff0c;如ReentrantLock、Semaphore、CountDownLatch等…

相关技术 太阳能热水器循环水泵制作技术

网盘 https://pan.baidu.com/s/1oAKwUEGkKnEgxE-F4znKow?pwdidxd 双温区蓄能供热型太阳能热水系统及其工作方法.pdf 双罐叠压节能恒温型太阳能热水机组.pdf 基于傅科电流的循环式风能热水器.pdf 基于太阳能利用的建筑冷热电联产系统及工作方法.pdf 基于太阳能和热泵的双蓄式热…

C++:特殊类的设计(无线程)

目录 一、设计一个不能拷贝类 二、设计一个只能在堆上创建对象的类 方法一&#xff1a;析构函数私有化 方法二&#xff1a;构造函数私有化 三、设计一个只能在栈上创建对象的类 四、设计一个类不能被继承 五、设计一个只能创建一个对象的类&#xff08;单例模式&#xf…

数学建模MATLAB绘图大全

最近快要开始一年一度的数学建模竞赛啦&#xff0c;接下来争取每天更一篇数学建模算法&#xff01;&#xff08;当然这是理想状态下&#xff09;&#xff0c;今天就先更一些MATLAB常用的绘图吧&#xff0c;论文赏心悦目的关键就在于丰富多彩的图&#xff0c;好看的图一定会成为…

景区智慧公厕,剩余厕位显示,公厕环境监测。

在当今科技飞速发展的时代&#xff0c;景区的服务设施也在不断升级和创新。其中&#xff0c;景区智慧公厕的出现无疑为游客带来了极大的便利。特别是剩余厕位显示和公厕环境监测这两大功能&#xff0c;更是显著提升了景区公厕的管理水平和游客的使用体验。 剩余厕位显示功能是景…

磁钢生产领域上下料解决方案

随着智能制造技术的不断革新&#xff0c;磁钢生产领域正逐步引入自动化生产线。然而&#xff0c;传统的人工上下料方式存在诸多问题&#xff0c;难以满足现代生产需求。富唯智能提出了一款复合机器人磁钢上下料解决方案&#xff0c;通过先进的自动化技术&#xff0c;提高生产效…

一款纯 js 实现的大模型应用服务 FastGPT 解读

背景介绍 最近被不同的人安利了 FastGPT 项目&#xff0c;实际上手体验了一下&#xff0c;使用流程类似之前调研过的 Dify, 包含的功能主要是&#xff1a;任务流的编排&#xff0c;知识库管理&#xff0c;另外还有一些外部工具的调用能力。使用页面如下所示&#xff1a; 实际…

前端实现坐标系转换

一、地理坐标系和投影坐标系 地理坐标系和投影坐标系是地理信息系统&#xff08;GIS&#xff09;中常见的两种坐标系统&#xff0c;它们用于描述和定位地球表面上的点和区域&#xff0c;但在实现方式和应用场景上有所不同。 1. 地理坐标系&#xff08;Geographic Coordinate …

【CentOS 7.6】Linux版本 portainer本地镜像导入docker安装配置教程,不需要魔法拉取!(找不着镜像的来看我)

吐槽 我本来根本不想写这篇博客&#xff0c;但我很不解也有点生气&#xff0c;CSDN这么大没有人把现在需要魔法才能拉取的镜像放上来。 你们都不放&#xff0c;根本不方便。我来上传资源。 portainer-ce-latest.tar Linux/amd64 镜像下载地址&#xff1a; 链接&#xff1a;h…

Chapter11让画面动起来——Shader入门精要学习笔记

Chapter11让画面动起来 一、Unity Shader中的内置变量&#xff08;时间篇&#xff09;二、纹理动画1.序列帧动画2.滚动背景 三、顶点动画1.流动的河流2.广告牌3.注意事项①批处理问题②阴影投射问题 一、Unity Shader中的内置变量&#xff08;时间篇&#xff09; Unity Shader…

东北财税之星:董女士的家乡创业记

乐财业智慧财税赋能平台&#xff0c;是一个帮助财税机构专业提升、业务增长&#xff0c;让财税生意更好做的综合赋能平台。聚焦财税公司业绩增长&#xff0c;预计2027年帮助2000家财税合伙人利润增长300%&#xff0c;致力打造轻量化、批量化、智能化的”业财税“一体财税服务生…