¿Cómo informar a la aplicación si está ejecutando pruebas unitarias en un proyecto Swift puro?


81

Una cosa molesta al ejecutar pruebas en Xcode 6.1 es que toda la aplicación tiene que ejecutarse e iniciar su guión gráfico y el controlador de vista raíz. En mi aplicación, esto ejecuta algunas llamadas al servidor que obtienen datos de la API. Sin embargo, no quiero que la aplicación haga esto al ejecutar sus pruebas.

Con las macros de preprocesador desaparecidas, ¿qué es lo mejor para que mi proyecto sepa que se lanzó ejecutando pruebas y no un lanzamiento normal? Los ejecuto normalmente con command+ Uy en un bot.

Pseudocódigo:

// Appdelegate.swift
if runningTests() {
   return
} else {
   // do ordinary api calls
}

"toda la aplicación tiene que ejecutarse y lanzar su guión gráfico y el controlador de vista raíz", ¿es correcto? No lo he probado pero no me parece correcto. Hmm ...
Fogmeister

Sí, la aplicación terminó de ejecutarse, así como viewdidload para el controlador de vista raíz
bogen

Ah, solo probado. No pensé que ese fuera el caso lol. ¿Qué tiene esto que está causando un problema en sus pruebas? ¿Quizás hay otra forma de evitarlo?
Fogmeister

Solo necesito que la aplicación sepa que se está ejecutando teniendo en cuenta las pruebas, por lo que una marca como las antiguas macros del preprocesador funcionaría, pero no son compatibles con Swift.
bogen

Sí, pero ¿por qué "necesitas" hacer eso? ¿Qué es lo que te hace pensar que necesitas hacer eso?
Fogmeister

Respuestas:


44

En lugar de verificar si las pruebas se están ejecutando para evitar efectos secundarios, puede ejecutar las pruebas sin la aplicación de host. Vaya a Configuración del proyecto -> seleccione el objetivo de prueba -> General -> Pruebas -> Aplicación de host -> seleccione 'Ninguno'. Solo recuerde incluir todos los archivos que necesita para ejecutar las pruebas, así como las bibliotecas que normalmente incluye el destino de la aplicación Host.

ingrese la descripción de la imagen aquí


1
¿Cómo incluir encabezados de puente para el destino después de eliminar la aplicación host?
Bhargav

Probé este, rompió mis pruebas hasta que llegué a esta respuesta que propone hacer todo lo contrario: stackoverflow.com/a/30939244/1602270
eagle.dan.1349

Si hago eso, puedo probar cloudKit (por ejemplo), por lo que la solución para mí es detectar en applicationDidFinishLaunching si estoy probando, y si la respuesta es SÍ, regresar sin asignar las clases principales de la aplicación.
user1105951

83

La respuesta de Elvind no es mala si quieres tener lo que solía llamarse "pruebas lógicas" puras. Si aún desea ejecutar su aplicación host contenedora pero ejecutar condicionalmente o no ejecutar código dependiendo de si se ejecutan pruebas, puede usar lo siguiente para detectar si se ha inyectado un paquete de prueba:

if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
     // Code only executes when tests are running
}

Utilicé una marca de compilación condicional como se describe en esta respuesta para que el costo de tiempo de ejecución solo se incurre en las compilaciones de depuración:

#if DEBUG
    if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
        // Code only executes when tests are running
    }
#endif

Editar Swift 3.0

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
    // Code only executes when tests are running
}

3
No funciona en el último Xcode - El nombre de la variable de entorno parece haber cambiado
amleszk

7
No estoy seguro exactamente cuándo dejó de funcionar, pero con Xcode 7.3 ahora estoy usando la XCTestConfigurationFilePathclave de entorno en lugar de XCInjectBundle.
ospr

Gracias @ospr, he editado la respuesta para trabajar con Xcode 7.3
Michael McGuire

@tkuichooseyou po ProcessInfo.processInfo.environmentno tiene clave XCTestConfigurationFilePath. ¿Puedes compartir tu código? Esto se comprueba contra un objetivo de UITest
Tal Zion

@TalZion lo siento, no me di cuenta de que te referías a un objetivo de UITest. ¿Probaste el NSClassFromString("XCTest")método siguiente?
tkuichooseyou

44

Yo uso esto en la aplicación: didFinishLaunchingWithOptions:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}

3
Esto solo se aplica a las pruebas unitarias y no a las nuevas pruebas de interfaz de usuario de Xcode 7.
Jesse

32

Otro, en mi opinión de forma más sencilla:

Edita su esquema para pasar un valor booleano como argumento de lanzamiento a su aplicación. Me gusta esto:

Establecer argumentos de lanzamiento en Xcode

Todos los argumentos de lanzamiento se agregan automáticamente a su NSUserDefaults.

Ahora puede obtener el BOOL como:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];

2
Parece que esta es la forma más limpia que he encontrado hasta ahora. No tenemos que agregar todos los archivos de la aplicación al objetivo de prueba y no tenemos que depender de alguna solución extraña para verificar "XCTestConfigurationFilePath"o NSClassFromString("XCTest"). Implementé esta solución en Swift con una función globalfunc isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") }
Kevin Hirsch

21

Creo que es completamente legítimo querer saber si se está ejecutando dentro de una prueba o no. Existen numerosas razones por las que esto puede resultar útil. Por ejemplo, al ejecutar pruebas, regreso antes de los métodos de ejecución de la aplicación / finalizará el lanzamiento en el Delegado de la aplicación, lo que hace que las pruebas comiencen más rápido para el código que no es pertinente para mi prueba unitaria. Sin embargo, no puedo hacer una prueba de pura "lógica", por una serie de otras razones.

Solía ​​usar la excelente técnica descrita por @Michael McGuire anteriormente. Sin embargo, noté que dejó de funcionar para mí alrededor de Xcode 6.4 / iOS8.4.1 (quizás se rompió antes).

Es decir, ya no veo el XCInjectBundle cuando ejecuto una prueba dentro de un objetivo de prueba para un marco mío. Es decir, estoy corriendo dentro de un objetivo de prueba que prueba un marco.

Entonces, utilizando el enfoque que sugiere @Fogmeister, cada uno de mis esquemas de prueba ahora establece una variable de entorno que puedo verificar.

ingrese la descripción de la imagen aquí

Luego, aquí hay un código que tengo en una clase llamada APPSTargetConfigurationque puede responderme esta simple pregunta.

static NSNumber *__isRunningTests;

+ (BOOL)isRunningTests;
{
    if (!__isRunningTests) {
        NSDictionary *environment = [[NSProcessInfo processInfo] environment];
        NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
        __isRunningTests = @([isRunningTestsValue isEqualToString:@"YES"]);
    }

    return [__isRunningTests boolValue];
}

La única advertencia con este enfoque es que si ejecuta una prueba desde el esquema de su aplicación principal, como XCTest le permitirá hacer (es decir, sin seleccionar uno de sus esquemas de prueba), no obtendrá este conjunto de variables de entorno.


5
en lugar de agregarlo en 'Ejecutar', ¿no sería más útil si lo agregamos en 'Prueba' para todos los esquemas? De esta forma, isRunningTests funcionará en todos los esquemas.
Vishal Singh

@VishalSingh Sí, creo que es más limpio. ¿Le diste una oportunidad a ese enfoque? Háganos saber si funcionó igual de bien para usted.
idStar

16
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

Uso

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"

A diferencia de otras respuestas, esta funciona. Gracias.
n13

8

Enfoque combinado de @Jessy y @Michael McGuire

(La respuesta aceptada no le ayudará mientras desarrolla un marco)

Así que aquí está el código:

#if DEBUG
        if (NSClassFromString(@"XCTest") == nil) {
            // Your code that shouldn't run under tests
        }
#else
        // unconditional Release version
#endif

1
¡Esto es mucho mejor! ¡Más corto y compatible con marcos!
blackjacx

Estaba trabajando en un paquete Swift y esto funcionó para mí
D. Greg

4

He aquí una forma que he estado usando en Swift 4 / Xcode 9 para nuestras pruebas unitarias. Está basado en la respuesta de Jesse .

No es fácil evitar que se cargue el guión gráfico, pero si agrega esto al comienzo de didFinishedLaunching, dejará muy claro a sus desarrolladores lo que está sucediendo:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions:
                 [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    #if DEBUG
    if let _ = NSClassFromString("XCTest") {
        // If we're running tests, don't launch the main storyboard as
        // it's confusing if that is running fetching content whilst the
        // tests are also doing so.
        let viewController = UIViewController()
        let label = UILabel()
        label.text = "Running tests..."
        label.frame = viewController.view.frame
        label.textAlignment = .center
        label.textColor = .white
        viewController.view.addSubview(label)
        self.window!.rootViewController = viewController
        return true
    }
    #endif

(¡Obviamente, no debería hacer nada como esto para las pruebas de IU en las que desea que la aplicación se inicie normalmente!)


3

Puede pasar argumentos en tiempo de ejecución a la aplicación según el esquema aquí ...

ingrese la descripción de la imagen aquí

Pero me preguntaría si realmente es necesario o no.


3

Esta es la forma rápida de hacerlo.

extension Thread {
  var isRunningXCTest: Bool {
    for key in self.threadDictionary.allKeys {
      guard let keyAsString = key as? String else {
        continue
      }

      if keyAsString.split(separator: ".").contains("xctest") {
        return true
      }
    }
    return false
  }
}

Y así es como lo usas:

if Thread.current.isRunningXCTest {
  // test code goes here
} else {
  // other code goes here
}

Aquí está el artículo completo: https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989


Ese bucle for puede ser Swiftilized así:return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }
Roger Oba

2

Algunos de estos enfoques no funcionan con UITests y si básicamente está probando con el código de la aplicación en sí (en lugar de agregar un código específico en un objetivo de UITest).

Terminé configurando una variable de entorno en el método de configuración de la prueba:

XCUIApplication *testApp = [[XCUIApplication alloc] init];

// set launch environment variables
NSDictionary *customEnv = [[NSMutableDictionary alloc] init];
[customEnv setValue:@"YES" forKey:@"APPS_IS_RUNNING_TEST"];
testApp.launchEnvironment = customEnv;
[testApp launch];

Tenga en cuenta que esto es seguro para mis pruebas, ya que actualmente no uso ningún otro valor de launchEnvironment; si lo hace, por supuesto, querrá copiar primero los valores existentes.

Luego, en el código de mi aplicación, busco esta variable de entorno si / cuando quiero excluir alguna funcionalidad durante una prueba:

BOOL testing = false;
...
if (! testing) {
    NSDictionary *environment = [[NSProcessInfo processInfo] environment];
    NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
    testing = [isRunningTestsValue isEqualToString:@"YES"];
}

Nota: gracias por el comentario de RishiG que me dio esta idea; Simplemente amplié eso a un ejemplo.


1

El método que había estado usando dejó de funcionar en Xcode 12 beta 1. Después de probar todas las respuestas basadas en la compilación a esta pregunta, me inspiré en la respuesta de @ ODB. Aquí hay una versión Swift de una solución bastante simple que funciona tanto para dispositivos reales como para simuladores. También debería ser bastante "a prueba de versiones".

Insertar en configuración de prueba:

let app = XCUIApplication()
app.launchEnvironment.updateValue("YES", forKey: "UITesting")
app.launch()

Insertar en la aplicación:

let isTesting: Bool = (ProcessInfo.processInfo.environment["UITesting"] == "YES")

Para usarlo:

    if isTesting {
        // Only if testing
    } else {
        // Only if not testing
    }

0

Trabajó para mi:

C objetivo

[[NSProcessInfo processInfo].environment[@"DYLD_INSERT_LIBRARIES"] containsString:@"libXCTTargetBootstrapInject"]

Rápido: ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libXCTTargetBootstrapInject") ?? false


0

Primero agregue la variable para probar:

ingrese la descripción de la imagen aquí

y usa eso en tu código:

 if ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1" {
                 // Code only executes when tests are running
 } 

0

Aparentemente, en Xcode12 necesitamos buscar en la clave de entorno en XCTestBundlePathlugar de XCTestConfigurationFilePathsi está utilizando el nuevoXCTestPlan

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.