Tyler-Keith-Thompson / CucumberSwift

A lightweight swift Cucumber implementation
https://tyler-keith-thompson.github.io/CucumberSwift/documentation/cucumberswift/
MIT License
75 stars 19 forks source link

Best Practices for Decoupling Step Files? #25

Closed cody1024d closed 3 years ago

cody1024d commented 3 years ago

Hello,

Based on my understanding of the project, all step definitions need to be defined in the extension of the Cucumber class. Do you have an example of how someone would extend this paradigm such that multiple files can contain the step definitions?

Bonus points if there is a way to do this via extension points that don't require access to the Cucumber class itself

Tyler-Keith-Thompson commented 3 years ago

Hello!

Yup, all steps have to have been defined by the time setupSteps finishes its execution but how you do that is up to you. Take a look at this discussion topic for a similar question and answer about how to use multiple step files.

To win your bonus points Given, When, Then, etc... are global functions, meaning you can execute them from anywhere (doesn't just have to be in the Cucumber extension). So if you wanted to use static methods, or dependency inject a class/enum/struct somewhere that did the work you could absolutely do that. Just make sure the steps are defined by the time setupSteps finishes.

Example:

extension Cucumber: StepImplementation {
    public var bundle: Bundle {
        class Findme { }
        return Bundle(for: Findme.self)
    }

    public func setupSteps() {
        MySteps.setupStepsForFeature1() //this declared in File 2
    }
}

(in File 2)

enum MySteps {
    static func setupStepsForFeature1() {
        Given("some precondition") { /* logic */ }
    }
}
cody1024d commented 3 years ago

Hey @Tyler-Keith-Thompson!

Thanks for the fast reply.

You've definitely won my bonus points! Hahaha. However, I think I've misspoken. What I really need is for the Step Implementation classes to be able to register themselves.

I essentially need a way to register functions/method calls to be executed on the execution of my test suite, so that all of the step-implementations get registered upon the beginning of the run, as hands-off as possible.

My use-case is probably a bit over the top, but I'm looking to make an internal framework that wraps around yours, and is then distributed to 20+ feature teams (after a POC) to do BDT with. Because of this, I am going for a very plug and play solution. The less chances for these developers to break something, the better. Another complication is that these frameworks/apps already have XCUITests, and so the integration is way easier if they're able to use/extend convenience classes/methods/test-cases that are already in the project.

I've got some hacky solutions, but wanted to get your opinion on what you think. My hacky solution involves embedding the step definitions into the setup methods of XCTestCases that have a single stubbed out method, so that they run, and so that the developers can use all of those convenience methods already implemented (and attached to XCTestCase heirarchies) to do validation, etc.

My initial thought, in terms of modifying this framework is to allow for more customizing during the XCTestSuite creation process. So that this way the available steps can be loaded from the contents of the xctestsuite, or more abstractly, from totally de-coupled step-implementation files.

Some way of being able to pick out step-implementations would be super helpful. For instance, on the Android/Kotlin side, it's done via reflection (finding classes that implement a certain interface/protocol)--- dirty, but it works. Perhaps allowing for a Info.plist entry, or something.

Wondering your thoughts on the above :)

Thanks again, Cody

Tyler-Keith-Thompson commented 3 years ago

Ooh okay! I'll play. As long as your construct is exposed to the objective-c runtime you can reflect in Swift. So if you're looking for a plugin pattern you could create an objective-c protocol (in swift) and the protocol could simply define a method where you can set your steps up.

If you're looking for the most developer friendly API you could also just use the same reflection XCTest already uses. So add a method to XCTestCase that gets called during that setupSteps method by your wrapper.... I'm not in a great position to give you working code but here's an example:

public extension XCTestCase {
    @objc func setupStepDefinitions() {} //NOTE this method is exposed to the objective-c runtime which let's us do my next trick
}

From the devs perspective

class SomeTests: XCTestCase {
    override func setupStepDefinitions() { //this works because Objective-C black magic nonsense
        Given("") { /* logic */ }
    }
}

Then in your wrapper:

#warning("I totally did not test this, it might not compile, but the idea works like this")
//Partially taken from: https://stackoverflow.com/questions/34415028/how-to-list-all-classes-conforming-to-protocol-in-swift
static var allTests: [XCTestCase] = {
    let expectedClassCount = objc_getClassList(nil, 0)
    let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
    let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
    let actualClassCount:Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)

    var classes = [AnyClass]()
    for i in 0 ..< actualClassCount {
        let currentClass = allClasses[Int(i)]
        if let c = class as? XCTestCase { //this statement might be totally wrong, there's probably a reflection method on objective-c for this, a little googling should get you there
           classes.append(c)
        }
    }

    return classes
}()
func setupSteps() {
    Self.allTests.forEach { $0.setupStepDefinitions() }
}
cody1024d commented 3 years ago

Ok very cool. After looking around today that's what I came up with too :)

In fact, I found that same exact SO answer hahaha

Just wanted to see what your thoughts were, as the creator :)

Also thanks for this framework----you've saved me ALOT of work haha