飞码网-免费源码博客分享网站

点击这里给我发消息

使用Detox进行React Native端到端测试和自动化|-JavaScript教程

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站

Detox是一个端到端测试和自动化框架,可以像实际的最终用户一样在设备或模拟器上运行。

软件开发要求快速响应用户和/或市场需求。这种快速的开发周期可能导致(很快或以后)项目的某些部分被破坏,尤其是当项目变得如此庞大时。开发人员对项目的所有技术复杂性不知所措,甚至商人也开始发现很难跟踪产品所适合的所有方案。

在这种情况下,需要使软件保持在项目之上并允许我们放心地进行部署。但是为什么要进行端到端测试?单元测试和集成测试还不够吗?为何还要烦恼端到端测试所带来的复杂性呢?

首先,大多数端到端框架已解决了复杂性问题,在某种程度上,某些工具(无论是免费,付费还是受限制)允许我们将测试记录为用户,然后重播并生成必要的代码。当然,这并不涵盖您能够以编程方式解决的所有情况,但这仍然是非常方便的功能。

端到端集成和单元测试

端到端测试与集成测试与单元测试:我总是发现“对抗”一词会驱使人们扎营-好像这是一场善与恶的战争。这驱使我们去扎营,而不是互相学习,而不是互相学习,而不是为什么。示例无数:Angular vs. React,React vs. Angular vs. Vue,甚至更多,React vs. Angular vs. Vue vs. Svelte。每个营地的垃圾彼此交谈。

jQuery通过利用外观模式$('')驯服野生的DOM野兽并使我全神贯注于手头的任务,从而使我成为一名更好的开发人员Angular通过将可重复使用的组件组成可组成指令(v1)的优势,使我成为了更好的开发人员。通过利用函数编程,不变性,身份引用比较以及其他框架中没有的可组合性级别,React使我成为了更好的开发人员。Vue通过利用反应式编程和推模型使我成为更好的开发人员。我可以继续说下去,但我只是想说明一点,我们需要更多地关注以下原因:为什么首先创建此工具,解决了哪些问题以及是否还有其他解决方法同样的问题。

当您上去时,您会获得更多的信心

端到端测试图,展示了端到端测试的优势及其带来的信心

随着您在模拟用户旅程方面的投入越来越多,您必须做更多的工作来模拟用户与产品的交互。但另一方面,您将获得最大的信心,因为您正在测试与用户交互的真实产品。因此,您抓住了所有问题-无论是样式问题,可能导致整个部分或整个交互过程不可见或不交互,内容问题,UI问题,API问题,服务器问题或数据库问题。您将获得所有这些覆盖,这使您充满信心。

为什么要排毒?

我们从一开始就讨论了端到端测试的好处及其在部署新功能或解决问题时提供最大信心的价值。但是为什么特别要排毒呢?在撰写本文时,它是React Native中最受欢迎的端到端测试库,并且是最活跃的社区。最重要的是,这是React Native在其文档中推荐的一种。

排毒测试理念是“灰盒测试”。灰盒测试是在框架了解其所测试产品内部的情况下进行的测试,换句话说,它知道它在React Native中,并且知道如何作为Detox进程的子进程来启动应用程序以及如何在Detox进程中重新加载它。每次测试后需要。因此,每个测试结果均独立于其他结果。

查看实际结果

首先,为了学习起见,我们克隆一个非常有趣的开源React Native项目,然后在其中添加Detox:

git clone https://github.com/ahmedam55/movie-swiper-detox-testing.git
cd movie-swiper-detox-testing
npm install
react-native run-ios

在Movie DB网站上创建一个帐户,以能够测试所有应用程序场景。然后分别.env使用usernamePlaceholder文件中添加用户名和密码passwordPlaceholder

isTesting=true
username=usernamePlaceholder
password=passwordPlaceholder

之后,您现在可以运行测试:

detox test

请注意,由于在detox-cli,detox和项目库之间进行了许多重大更改,因此我不得不从原始库中派生此仓库。使用以下步骤作为操作基础:

  1. 将其完全迁移到最新的React Native项目。
  2. 更新所有库以修复Detox在测试时面临的问题。
  3. 如果环境正在测试,则切换动画和无限计时器。
  4. 添加测试套件包。

设置新项目

将排毒添加到我们的依赖中

转到项目的根目录并添加Detox:

npm install detox --save-dev

配置排毒

打开package.json文件,并在项目名称config之后添加以下内容。确保movieSwiper在iOS配置中用您的应用名称替换在这里,我们告诉Detox在哪里可以找到二进制应用程序以及构建它的命令。(这是可选的,我们可以随时执行。react-native run-ios代替。)也可以选择模拟器的哪个类型:ios.simulatorios.noneandroid.emulator,或android.attached并选择要测试的设备:

{
  "name": "movie-swiper-detox-testing",

  // add these:
  "detox": {
    "configurations": {
      "ios.sim.debug": {
        "binaryPath": "ios/build/movieSwiper/Build/Products/Debug-iphonesimulator/movieSwiper.app",
        "build": "xcodebuild -project ios/movieSwiper.xcodeproj -scheme movieSwiper -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
        "type": "ios.simulator",
        "name": "iPhone 7 Plus"
      }
    }
  }
}

以下是上述配置的详细信息:

  • 执行react-native run-ios以创建二进制应用程序。
  • 在项目的根目录下搜索二进制应用程序:find . -name "*.app"
  • 将结果放入build目录。

在启动测试套件之前,请确保name您指定的设备可用(例如,iPhone 7)。您可以通过执行以下操作从终端执行此操作:

 
xcrun simctl list

看起来是这样的:

设备列表

现在,我们已经将Detox添加到我们的项目中,并告诉它启动该应用程序的模拟器,我们需要一个测试运行器来管理断言和报告-无论是在终端上还是在其他地方。

排毒同时支持Jest和Mocha。我们将选择Jest,因为它具有更大的社区和更大的功能集。除此之外,它还支持并行测试执行,随着数量的增加,可以加快端到端测试的速度。

 

 

添加这是开发依赖

执行以下步骤安装Jest:

npm install jest jest-cli --save-dev

生成测试套件文件

要将Detox初始化为使用Jest,请执行以下操作:

detox init -r jest

这将e2e在项目的根目录以及其中的以下内容创建一个文件夹:

  • e2e / config.json包含测试运行程序的全局配置:

      {
          "setupFilesAfterEnv": ["./init.js"],
          "testEnvironment": "node",
          "reporters": ["detox/runners/jest/streamlineReporter"],
          "verbose": true
      }
    
  • e2e / init.js包含在执行任何测试之前运行的初始化代码:

    const detox = require('detox');
      const config = require('../package.json').detox;
      const adapter = require('detox/runners/jest/adapter');
      const specReporter = require('detox/runners/jest/specReporter');
    
      // Set the default timeout
      jest.setTimeout(25000);
      jasmine.getEnv().addReporter(adapter);
    
      // This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
      // This is strictly optional.
      jasmine.getEnv().addReporter(specReporter);
    
      beforeAll(async () => {
        await detox.init(config);
      });
    
      beforeEach(async () => {
        await adapter.beforeEach();
      });
    
      afterAll(async () => {
        await adapter.afterAll();
        await detox.cleanup();
      });
    
  • e2e / firstTest.spec.js是默认的Detox测试文件。这是我们将对应用程序进行所有测试的地方。我们将详细讨论describeit块,以及稍后将要创建的测试套件。

最后,我们进行测试

要运行测试,请导航到项目的根目录并执行以下操作:

detox test

恭喜你!我们已经准备好一切,可以编写出色的测试。您可以根据需要创建和管理任意数量的e2e/*spec.js文件,测试运行程序将一一执行它们。规格文件表示您要测试的一组独立功能。例如,结帐,访客结帐,用户身份验证或注册。

在spec文件中,您将拥有describe它包含it为读取而创建的最小测试块-块。例如:it should reject creating an account if name already exits在该it块中,添加必要的断言以确保这是正确的。理想情况下,我们应该在每个it之后重新加载React Native 只要他们不互相依赖即可。这样可以防止误报,并使调试更加容易。知道此测试在干净的条件下失败了,因此您不必担心所有其他情况。

深入我们的测试套件

我们将检查应用程序是否满足以下情况。

  • 它应该禁止使用错误的凭据登录这似乎很明显,但是对应用程序工作流程至关重要,因此需要对每次更改和/或部署进行测试。
  • 它应该使用有效的凭据对用户进行身份验证-测试身份验证功能是否正常运行。
  • 当用户退出时,它应该踢出用户-测试退出是否使用户离开“浏览”,“浏览”和“库”屏幕。
  • 它应该只允许访客浏览屏幕用户可以以访客身份登录或继续,在这种情况下,他们只能访问“浏览”屏幕及其功能。
  • 它应获取与查询匹配的电影-测试渲染的电影是否与搜索查询匹配的电影。
  • 它应该添加到收藏夹中—测试“添加到收藏夹电影”功能,并确保添加的电影出现在“收藏夹电影”列表中。
  • 它应该添加到监视列表中-与测试添加到喜欢的电影类似,但具有监视列表功能。
  • 单击更多按钮时,它应该显示所有内容-测试“浏览”部分的“更多”按钮功能:
    • 每日趋势
    • 每周趋势
    • 流行
    • 最高评分
    • 确保它导航到具有所有符合所选条件的电影的电影列表视图。

遍历测试套件的代码

现在是时候让我们回顾一下测试应用程序的代码了。不过,在此之前,我建议您先在设备或模拟器上运行该应用程序。这是为了使您熟悉应用程序中的不同屏幕和UI组件。

我们需要做的第一件事是定义将用于执行各种测试的功能。当我发现自己匹配同一组UI元素并执行一组特定的操作时,我将其抽象为自己的功能,因此可以在其他测试中重用它,并将修复和更改集中在一个地方。以下是一些对我有所帮助的抽象示例:

  • loginWithWrongCredentials()
  • loginWithRightCredentials()
  • goToLibrary()
  • signOut()
  • searchForMovie(title)

即使您以前没有使用过Detox的API,对您来说也应该很容易理解。这是代码:

 

 

// e2e/firstTestSuite.spec.js

// fetch the username and password from the .env file
const username = process.env.username;
const password = process.env.password;

const sleep = duration =>
  new Promise(resolve => setTimeout(() => resolve(), duration)); // function for pausing the execution of the test. Mainly used for waiting for a specific UI component to appear on the screen

const loginWith = async (username, password) => {
  try {
    // click on login btn to navigate to the username, password screen
    const navigateToLoginBtn = await element(by.id("navigate-login-btn"));
    await navigateToLoginBtn.tap();

    const usernameInput = await element(by.id("username-input"));
    const passwordInput = await element(by.id("password-input"));

    await usernameInput.tap();
    await usernameInput.typeText(username);
    await passwordInput.typeText(password);

    const loginBtn = await element(by.id("login-btn"));

    await loginBtn.tap(); // to close the keyboard
    await loginBtn.tap(); // to start the authentication process

    const errorMessage = await element(
      by.text("Invalid username and/or password")
    );

    return { errorMessage, usernameInput, passwordInput };
  } catch (e) {
    console.log(
      "A sign out has not been done, which made the `navigate-login-btn` not found"
    );
  }
};

const loginWithWrongCredentials = async () =>
  await loginWith("alex339", "9sdfhsakjf"); // log in with some random incorrect credentials
const loginWithRightCredentials = async () =>
  await loginWith(username, password); // log in with the correct credentials

const goToLibrary = async () => {
  const libraryBtn = await element(by.id("navigation-btn-Library"));
  await libraryBtn.tap();
};

const goToExplore = async () => {
  const exploreBtn = await element(by.id("navigation-btn-Explore"));
  await exploreBtn.tap();
};

const signOut = async () => {
  await goToLibrary();

  const settingsBtn = await element(by.id("settings-btn"));
  await settingsBtn.tap();

  const signOutBtn = await element(by.id("sign-out-btn"));
  await signOutBtn.tap();
};

const continueAsGuest = async () => {
  const continueAsGuestBtn = await element(by.id("continue-as-guest"));
  await continueAsGuestBtn.tap();
};

const searchForMovie = async movieTitle => {
  const searchMoviesInput = await element(by.id("search-input-input"));
  await searchMoviesInput.tap();
  await searchMoviesInput.clearText();
  await searchMoviesInput.typeText(movieTitle);
};

const goBack = async () => {
  const goBackBtn = await element(by.id("go-back-btn"));
  goBackBtn.tap();
};

const goToWatchListMovies = async () => {
  const watchListBtn = await element(by.id("my-watchlist"));
  await watchListBtn.tap();
};

const goToFavoriteMovies = async () => {
  const favoriteMoviesBtn = await element(by.id("my-favorite-movies"));
  await favoriteMoviesBtn.tap();
};

const clickFavoriteButton = async () => {
  const addToWatchListBtn = await element(by.id("add-to-favorite-btn"));
  await addToWatchListBtn.tap();
};

const clickWatchListButton = async () => {
  const addToWatchListBtn = await element(by.id("add-to-watch-list-btn"));
  await addToWatchListBtn.tap();
};

const removeTestMoviesFromLists = async () => {
  try {
    await loginWithRightCredentials();
    await goToLibrary();
    await goToWatchListMovies();

    const movieItemInWatchList = await element(
      by.text("Crazy Rich Asians").withAncestor(by.id("watch-list"))
    );

    await movieItemInWatchList.tap();
    await clickWatchListButton();
    await goToLibrary();
    await goToFavoriteMovies();

    const movieItemInFavorites = await element(
      by.text("Avengers: Endgame").withAncestor(by.id("favorite-list"))
    );

    await movieItemInFavorites.tap();
    await clickFavoriteButton();
  } catch (e) {}
  await signOut();
};

// next: add function for asserting movie items

接下来,我们添加用于声明电影项目的函数。与上面定义的所有其他功能不同,此功能实际上是在运行单独的测试-断言特定的电影项目在屏幕上可见:

const assertMovieItems = async (moviesTitles = []) => {
  for (let i = 0; i < moviesTitles.length; i++) {
    const moviesItem = await element(by.text(moviesTitles[i]));
    await expect(moviesItem).toBeVisible();
  }
};

// next: create the test suite

至此,我们现在准备创建测试套件。这应该包装在一个describe块中。为了使每个测试都有一个“干净”的起点,我们使用以下生命周期方法:

  • beforeAll:在此测试套件运行之前执行一次。在这种情况下,我们调用该removeTestMoviesFromLists()函数。如您先前所见,这等效于启动检查序列,在该序列中,用户登录并访问各种页面,然后单击将在测试中使用的各种按钮。这样可以确保该应用在开始运行测试之前处于最低功能状态。
  • beforeEach:在此测试套件中的每个测试运行之前执行。在这种情况下,我们要重新加载React Native。注意,这与按同样的效果+ rrrCtrl+r键盘上。
  • afterEach:在此测试套件中的每个测试运行后执行。在这种情况下,我们要注销用户,这意味着在我们的每个测试中,我们都需要重新登录用户。再次,这是在编写测​​试时要考虑的一个好习惯:每个测试都必须具有相同的起点。这样可以确保它们可以按任何顺序运行,并且仍然产生相同的结果:
    describe("Project Test Suite", () => {
        beforeAll(async () => {
          await removeTestMoviesFromLists();
        });
    
        beforeEach(async () => {
          await device.reloadReactNative();
        });
    
        afterEach(async () => {
          try {
            await signOut();
          } catch (e) {}
        });
    
        // next: run the individual tests
      });
      

现在让我们来看一下各个测试。这些可以在it内定义每个it块都从一开始就断言,并断言一个特定的,定义明确的场景(我们在上一节中介绍的场景)。每个测试都有可预测的输出,这是我们需要声明的内容:

it("should disallow login with wrong credentials", async () => {
  const {
    errorMessage,
    usernameInput,
    passwordInput
  } = await loginWithWrongCredentials();

  await expect(errorMessage).toBeVisible();
  await expect(usernameInput).toBeVisible();
  await expect(passwordInput).toBeVisible();
});

it("should login with right credentials", async () => {
  await loginWithRightCredentials();

  await goToLibrary();

  const watchListBtn = element(by.id("my-watchlist"));
  const favoriteMoviesBtn = element(by.id("my-favorite-movies"));

  await expect(watchListBtn).toBeVisible();
  await expect(favoriteMoviesBtn).toBeVisible();
});

it("should kick user out when sign out is clicked", async () => {
  await loginWithRightCredentials();
  await goToLibrary();
  await signOut();

  const loginBtn = await element(by.id("navigate-login-btn"));
  await expect(loginBtn).toBeVisible();
});

it("should allow guest in for Browse only", async () => {
  await continueAsGuest();
  await goToLibrary();

  const watchListBtn = element(by.id("my-watchlist"));
  const favoriteMoviesBtn = element(by.id("my-favorite-movies"));

  await expect(watchListBtn).toBeNotVisible();
  await expect(favoriteMoviesBtn).toBeNotVisible();

  await goToExplore();

  const moviesSwipingView = element(by.id("movies-swiping-view"));

  await expect(moviesSwipingView).toBeNotVisible();
});

it("should fetch and render the searches properly", async () => {
  await loginWithRightCredentials();

  const searches = [
    {
      query: "xmen",
      results: ["X-Men: Apocalypse", "X-Men: Days of Future Past"]
    },
    {
      query: "avengers",
      results: ["Avengers: Endgame", "Avengers: Age of Ultron"]
    },
    { query: "wolverine", results: ["Logan", "The Wolverine"] }
  ];

  for (let i = 0; i < searches.length; i++) {
    const currentSearch = searches[i];

    await searchForMovie(currentSearch.query);
    await assertMovieItems(currentSearch.results);
  }
});

it("should add to favorite", async () => {
  await loginWithRightCredentials();

  await searchForMovie("avengers");
  await element(by.text("Avengers: Endgame")).tap();

  await clickFavoriteButton();
  await goBack();
  await goToLibrary();
  await goToFavoriteMovies();

  await sleep(3000);

  var movieItemInFavorites = await element(
    by.id("favorite-list").withDescendant(by.text("Avengers: Endgame"))
  );

  await expect(movieItemInFavorites).toBeVisible();
});

it("should add to watchlist", async () => {
  await loginWithRightCredentials();

  await searchForMovie("crazy rich");
  await element(by.text("Crazy Rich Asians")).tap();

  await clickWatchListButton();

  await goBack();
  await goToLibrary();
  await goToWatchListMovies();

  await sleep(3000);

  const movieItemInFavorites = await element(
    by.id("watch-list").withDescendant(by.text("Crazy Rich Asians"))
  );

  await expect(movieItemInFavorites).toBeVisible();
});

it("should show all lists more is clicked", async () => {
  await loginWithRightCredentials();

  const trendingDailyMoreBtn = await element(by.id("trending-daily-more"));
  await trendingDailyMoreBtn.tap();

  await goBack();
  await sleep(300);

  const trendingWeeklyMoreBtn = await element(by.id("trending-weekly-more"));
  await trendingWeeklyMoreBtn.tap();

  await goBack();
  await sleep(300);

  const popularMoreBtn = await element(by.id("popular-more"));
  await popularMoreBtn.tap();

  await goBack();
  await sleep(300);

  const browseSectionsView = await element(by.id("browse-sections-view"));
  await browseSectionsView.scrollTo("bottom");

  const topRatedMoreBtn = await element(by.id("top-rated-more"));
  await topRatedMoreBtn.tap();
});

从上面的代码中,您可以看到每个测试的工作流程可以归纳为四个步骤:

  1. 初始化状态这是我们登录用户的位置,因此每个测试都有相同的起点。
  2. 选择UI组件在这里,我们使用匹配器来定位特定的UI组件。
  3. 触发动作这是我们在选定的UI组件上触发操作的地方。
  4. 断言预期的输出存在或不存在在这里,我们使用该expect()方法来测试该动作是否触发了另一个UI组件以在屏幕上显示还是隐藏。如果断言返回true,则测试通过。

注意:由于应用程序的不断变化的性质,我们断言的电影项目可能会非常频繁地更改。如果您是在这篇文章出版后的某个时间阅读本文,请确保首先手动确认屏幕上是否显示特定项目。这有助于避免测试不必要地失败,并且可以避免使演示工作的麻烦。

匹配器

您可以按ID,文本,标签,父项,子项(在任何级别)或特征来匹配或选择任何UI元素。以下是几个示例:

const usernameInput = await element(by.id("username-input"));
const errorMessage = await element(by.text("Invalid username and/or password"));

要执行的动作

排毒可以在UI元素进行了巨大的一套动作:taplongPressmultiTaptapAtPointswipetypeTextclearTextscrollscrollTo,和其他人。

这里有一些例子:

await usernameInput.tap();

await usernameInput.typeText(username);

await passwordInput.clearText();

const browseSectionsView = await element(by.id("browse-sections-view"));

await browseSectionsView.scrollTo("bottom");

断言测试

排毒具有一组断言,可以针对匹配的UI元素来执行:toBeVisibletoNotBeVisibletoExisttoNotExisttoHaveTexttoHaveLabeltoHaveIdtoHaveValue这是几个示例:

const assertMovieItems = async (moviesTitles = []) => {
  for (let i = 0; i < moviesTitles.length; i++) {
    const moviesItem = await element(by.text(moviesTitles[i]));
    await expect(moviesItem).toBeVisible();
  }
};

await assertMovieItems(["Avengers: Endgame", "Avengers: Age of Ultron"]);
const watchListBtn = element(by.id("my-watchlist"));
await expect(watchListBtn).toBeNotVisible();

挑战与食谱

无尽的循环动画或计时器

我面临的问题之一是,如果有计时器循环或动画永无休止,排毒会暂停。我必须执行以下操作才能调试此类问题:

  1. 在应用程序树中搜索和调试零件,并通过修改和消除零件进行导入。
  2. 再次运行测试套件,以检查问题是否仍然存在。
  3. 在那之后的大部分时间里,问题是动画在完成后立即开始播放。因此,我导入了react-native-config,这是一个非常方便的工具,用于设置一些环境变量以根据环境切换某些行为或功能。就我而言,它是isTesting=true.env文件中添加在代码库中检查它并禁用动画循环或使持续时间缩短很多,因此它加快了测试套件的速度。

如您所见,这主要是玩弄应用程序中的动画设置。有关排毒问题的更多信息,您可以查看以下文档:

  • 安装疑难解答
  • 同步故障排除
  • 对失败的测试进行故障排除
  • 故障诊断

将TestID添加到适当的UI元素

另一个挑战是挖掘组件以将其传递testID给,因为Detox不支持自定义组件。有时,您需要使用内置组件(例如,组件)包装该组件,View以便进行匹配并与其进行交互。如果内部内置组件的代码是node_modules文件夹内的导入库,则尤其如此

将TestID与上下文数据组合

我必须处理的另一种情况是使用不同的事件处理程序和标题在多个位置呈现的组件。因此,我必须创建一个testID包含标题,小写字母和连字符的组合,以及testID组件标识符。

例如,所有浏览部分more按钮:因为每个部分都呈现相同的组件:

 const testID = `${(this.props.title||'').toLowerCase().replace(/\s/g, '-')}-more`

 return (
  ...
    <AppButton
       onlyText
       style={styles.moreButton}
       textStyle={styles.moreButtonText}
       onPress={this.onMorePress}
       testID={testID}
    >
       MORE
    </AppButton>
 }

有时,它不是单个道具,而是子项,因此最终需要过滤它们并映射它们以获得文本节点及其值。

缩小选择器

由于某些导航员倾向于将先前的屏幕保留在树​​中,因此Detox会找到两个具有相同标识符(文本,ID,标签)的项目,并抛出异常。因此,我们需要从特定屏幕中过滤出项目以获取所需的内容。您可以使用withAncestor()匹配器(通过特定的祖先ID进行匹配)来做到这一点

const movieItemInWatchList = await element(
  by.text("Crazy Rich Asians").withAncestor(by.id("watch-list"))
);

await movieItemInWatchList.tap();

让我们以更吸引人的方式看结果

您可以查看下面运行的测试的屏幕记录。当运行应用程序的测试时,您应该会得到类似的结果。

 

为了模拟文本键入,选择输入时必须出现键盘要启用该功能,请转到“模拟器”>“键盘”>“切换软件键盘”您应该在开始运行测试之前执行此步骤。

结论

在本教程中,您学习了如何使用Detox在React Native应用程序中实现端到端测试。

具体来说,您学习了如何添加Detox配置以在iOS上运行测试,如何编写用于与UI组件交互的选择器,以及在与UI交互后断言屏幕上存在特定内容。最后,您了解了您可能会遇到的一些最常见的挑战以及如何解决它们。

在本教程中,我们仅针对iOS进行了测试,但是您也应该能够在Android上运行测试。请注意,您可能必须将应用程序降级到较低版本的React Native和Detox,才能在Android上运行。这是因为Detox对iOS的支持更好。

您可以在此GitHub存储库上查看源代码。

飞码网-免费源码博客分享网站 爱上飞码网—https://www.codefrees.com— 飞码网-matlab-python-C++ 爱上飞码网—https://www.codefrees.com— 飞码网-免费源码博客分享网站
赞 ()
内容页底部广告位3
留言与评论(共有 0 条评论)
   
验证码: