2017-07-20 1 views
1

Disons que nous avons ce ENUMÉviter le couplage lors de l'utilisation ENUM dans les tests

enum Action: String { 
    case doThing 
    case doOtherThing 
} 

Ce ENUM est utilisé de cette façon:

func run(action: Action, block:() -> Void) 

Maintenant, je test unitaire la méthode run donc je dois passer un Action ainsi:

func testActionRun() { 

    let expect = expectation(description: #function) 
    let sut = ActionRunner() 

    sut.run(action: .doThing) { 
     expect.fulfill() 
     // Assert something 
    } 

    waitForExpectations(timeout: 0.1, handler: nil) 
} 

Comme je l'ai besoin de tester d'autres situations sur ActionRunner, j'ai terminé avec beaucoup de .doThing répartis sur l'ensemble de la suite de tests.

Le problème est: si je fais une modification dans le code de production et change case doThing en case doThatThing maintenant toute ma suite de tests échoue parce qu'il n'y a pas case doThing.

L'idéal serait de déclarer un case factice dans le code de test pour permettre quelque chose comme

sut.run(action: .dummyAction) { 
} 

mais enum ne permet pas car elle ne permet ni l'héritage d'une extension d'ajouter un case.

La première option qui m'est venue à l'esprit était de convertir Action en protocole, mais ce changement est inutile en production et son seul but est d'accomplir quelque chose dans le code de test.

Alors, existe-t-il une autre option pour y parvenir?

Répondre

2

La question de savoir comment éviter le couplage lors de l'utilisation d'énumérations est délicate. Je suis tombé sur cela moi-même quelques fois sans réponse solide:/

Un point que vous soulevez est celui d'utiliser un protocole, et cela se sent inutile dans la production. Je suis en quelque sorte d'accord avec cela, mais la plupart du temps c'est le mal nécessaire.

Dans l'exemple que vous avez montré bien que je pense que peut-être un réglage dans la conception pourrait résoudre une partie du problème.

En particulier quand on regarde ce code

func run(action: Action, block:() -> Void) { 
    // ... 
} 

func testActionRun() { 

    let expect = expectation(description: #function) 
    let sut = ActionRunner() 

    sut.run(action: .doThing) { 
     expect.fulfill() 
     // Assert something 
    } 

    waitForExpectations(timeout: 0.1, handler: nil) 
} 

Ce qui vient à l'esprit pour moi est que votre Action spécifie un certain comportement. C'est lorsque vous testez la méthode run en passant .doThing que vous attendez un comportement différent que lorsque vous passez .

Si c'est vrai, y a-t-il une raison pour laquelle vous devez passer l'action enum instance et un bloc d'action à la fonction run?

Vous pourriez séparer le code qui définit le comportement de celui qui exécute l'action réelle encore plus que ce que vous avez déjà fait. Par exemple:

protocol Actionable { 
    var action:() ->() { get } 
} 

enum Action: Actionable { 
    case doThing 
    case doOtherThing 

    var action { 
     switch self { 
     case .doThing: return ... 
     case .doOtherThing: return ... 
    } 
} 

class ActionRunner { 
    func run(actionable: Actionable) { 
     actionable.action() 
    } 
} 

func testActionRun() { 
    let expect = expectation(description: #function) 
    let sut = ActionRunner() 

    sut.run(actionable: FakeActionable()) { 
     expectation.fulfill() 
    } 

    waitForExpectations(timeout: 0.1, handler: nil) 
} 

class FakeActionable: Actionable { 
    let action = { } 
} 

func testDoThing() { 
    let sut = Action.doThing 

    sut.action() 

    // XCTAssert for the expected effect of the action 
} 

Note: Je ne l'ai pas fait compilé ce code, donc porter avec moi si elle a des erreurs. Cela devrait donner l'idée cependant.

De cette façon, vous avez ActionRunner qui seul but est de fonctionner correctement une donnée Actionable et le Action ENUM seul but est de décrire les différentes actions devraient faire.

Cet exemple de code est plutôt restreint dans ce qu'il peut faire, exécutez seulement () ->() actions, mais vous pouvez construire sur le dessus pour atteindre des comportements plus avancés.

+0

Merci beaucoup @mokagio. Cela me semble parfait, il découle très élégamment une chose d'une autre. – emenegro

+0

Je suis content que vous ayez aimé ma suggestion @emenegro, laissez-moi savoir comment ça se passe :) – mokagio

1

Si vous modifiez votre code de production, vous devez également modifier votre code de test afin de tester ces nouveaux changements.

Peut-être que vous pouvez définir la valeur d'une variable Action dans le menu FUNC setUp de votre XCTestCase classe

import XCTest 

class SharingKitTests: XCTestCase { 
    var theAction: Action! 

    override func setUp() { 
     super.setUp() 

     self.theAction = .doThing 

    } 
} 

Ensuite, vous serez en mesure d'utiliser cette theAction var dans toutes vos méthodes d'essai, et si vous besoin de changer la valeur dont vous avez seulement besoin pour le changer en un seul endroit.

+0

C'est une bonne approche mais ce n'est pas le point en soi. Il est ennuyeux de changer chaque occurrence de '.doThing' mais j'utiliserais find & replace. Aussi, quand j'écris des tests, je préfère appliquer DAMP au lieu de DRY. – emenegro

+0

Je n'avais jamais entendu parler de DAMP. Merci @emenegro pour le mentionner :) – mokagio