Получение доступа к командной строке из XCTest


  • Что это такое?

  • И это полезно, потому что…?

  • Круто, в чём прикол?

  • Хорошо, покажи мне, как это сделать!

  • Это работает на реальных устройствах?

  • Хорошо, это работает на CI?

  • Бесконечно благодарен!

Что это такое?

Командная строка — довольно мощный инструмент. Его можно использовать для автоматизации множества вещей здесь и там. Очевидно, речь идёт не только о сценариях Shell, из него можно запустить и любой другой инструмент. Но использовать его напрямую не всегда возможно.

XCTest, например, работает внутри песочницы, это означает, что вы буквально не можете даже высунуть нос из коробки. Ну, почти. Давайте обойдём это ограничение и узнаем, как получить доступ к командной строке из тестов.

И это полезно, потому что…?

1. Это открывает совершенно новый мир функций, связанных с симулятором, которые доступны через simctl и idb, но еще не через XCTest API. Например:

  • тестирование push-уведомлений;

  • автоматизация сценариев, связанных с местоположением;

  • видеозапись неудачных испытаний.

2. Это открывает возможность создать и использовать автономный фиктивный сервер, который может полностью заменить реальный бэкэнд для целей тестирования.

3. Это позволяет выполнять любые действия пользовательского интерфейса macOS через Applescript из ваших XCTests.

4. Это дает возможность автоматизировать действия, связанные с Интернетом, с помощью playwright или selenium вместо открытия Safari на iOS.

5. Это предлагает довольно гибкий способ экспортировать всё, что вы хотите, из теста на хост-компьютер (скриншоты, макеты, тестовые данные и т. д.).

6. И многое другое.

Круто, в чём прикол?

Нам необходимо создать HTTP-сервер, который будет прослушивать запросы. Затем мы можем отправить запрос из XCTest на сервер и запустить всё, что захотим. Сервер может даже вернуть выходные данные обратно в XCTest, если это необходимо.

Хорошо, покажи мне, как это сделать!

Пример сценария

GIVEN we have a server running on localhost4567 (у нас есть сервер, работающий на локальном хосте: 4567)

AND we sent a request to /terminal endpoint from XCTest with the desired command (и мы отправили запрос на конечную точку /terminal из XCTest с необходимой командой)

WHEN server receives the request (сервер получает запрос)

AND server executes the command (и сервер выполняет команду)

THEN server returns the result back to XCTest (тогда сервер возвращает результат обратно в XCTest)

AND XCTest verifies the result (и XCTest проверяет результат)

Проходим сценарий

1. Выберите любой веб-фреймворк по вашему выбору (они почти все одинаковы для нашего варианта использования)

{
   "js": "express", // https://github.com/expressjs/express
   "rb": "sinatra", // https://github.com/sinatra/sinatra
   "py": "flask", // https://github.com/pallets/flask
   "go": "gin", // https://github.com/gin-gonic/gin
   "..": "..." // ...
 }

2. Создайте HTTP-сервер (в качестве примера я взял { "js": "express" })

  • Командная строка:

npm install express
touch server.js
open server.js
  • server.js:

const process = require("child_process");
  const express = require("express");
  const app = express();
  const port = 4567;

  app.use(express.json());

  app.listen(port);

  app.post("/terminal", (req, res) => {
    const output = exec(req.body.command, req.query.async).toString("utf8").trim();
    res.send(output);
  });

  function exec(command, async) {
    if (async === "true") {
      return process.exec(command);
    } else {
      return process.execSync(command);
    }
  }

3. Запустите сервер

4. Создайте тестовый файл

import XCTest

 class SampleUITests: XCTestCase {
     let host = "http://localhost"
     let port: UInt16 = 4567

     func testMacintoshVersion() {
         let macOsVersion = exec("sw_vers -productVersion")
         XCTAssertEqual(macOsVersion.split(separator: ".").first, "12")
     }

     func exec(_ command: String, async: Bool = false) -> String {
         let urlString = "\(host):\(port)/terminal?async=\(async)"
         guard let url = URL(string: urlString) else { return "" }

         var request = URLRequest(url: url)
         request.httpMethod = "POST"
         request.setValue("application/json", forHTTPHeaderField: "Content-Type")
         request.httpBody = try? JSONSerialization.data(withJSONObject: ["command": command], options: [])
         var output = ""

         if async {
             // Do not wait for the command to complete
             URLSession.shared.dataTask(with: request).resume()
         } else {
             // Wait for the command to complete
             let semaphore = DispatchSemaphore(value: 0)
             let task = URLSession.shared.dataTask(with: request) { data, response, error in
                 if let data = data, let string = String(data: data, encoding: .utf8) {
                     output = string
                     semaphore.signal()
                 }
             }
             task.resume()
             semaphore.wait()
         }
         return output
     }
 }

5. Запустите тест

6. Настройте скрипты под свои нужды и наслаждайтесь взломом!

Это работает на реальных устройствах?

Эмм, да, но не из коробки. Вам необходимо убедиться, что сервер доступен с устройства.Для этого есть два варианта:

  • Подключайтесь к Wi-Fi и отправляйте запросы на сервер, используя IP-адрес рабочего стола вместо локального хоста.

  • Предоставьте доступ к вашему локальному серверу в Интернете (например, через ngrok).

Хорошо, а это работает на CI?

Конечно, работает как часы!

Просто убедитесь, что сервер запущен в фоновом режиме на CI:

node server.js &

Бесконечно благодарен!

Мы Вам более чем рады!




Комментарии (1):

  1. SClown
    /#25196396

    Надеялся найти как запсутить web-server из командной строки из теста. А оно вон как обернулось)