lefthandedgoat / canopy

f# web automation and testing library, built on top of Selenium (friendly to c# also)
http://lefthandedgoat.github.io/canopy/
MIT License
506 stars 115 forks source link

How to pass browser instance to functions for parallel functions tests? #520

Closed jamesaslett1985 closed 3 years ago

jamesaslett1985 commented 3 years ago

Description

I am trying to get parallel execution working with my existing canopy framework. The examples in functionsTest.fs obviously work fine, however I'm having a hard time figuring out how to correctly pass the browser instance into each of my existing functions. I'm using NUnit but I don't think that's relevant to my issue:

Example

open NUnit.Framework
open BaseClass
open canopy.parallell.functions

[<TestFixture>]
type CanopyExample() =
    inherit BaseClass()

    [<TestCase(TestName="CanopyExample1", Category="Parallel Tests")>]
        member this.CanopyExample1() =
            let testpage = "http://lefthandedgoat.github.io/canopy/testpages/"
            let browser = start canopy.types.Chrome
            url testpage browser

            //works
            click "#button" browser

            let myButtonFunc browser = "#button"
            //Message=Can't click CanopyExample+button@19T[System.Object] because it is not a string or webelement
            click myButtonFunc browser

            //click not performed
            //FS0193: This expression is a function value, i.e. is missing arguments. Its type is 'a -> unit
            myButtonFunc browser |> click

I also have another function which worked fine before, but now I get Can't perform the action because the browser instance is null'.

Any help greatly appreciated, thanks :)

lefthandedgoat commented 3 years ago

let myButtonFunc browser = "#button" is a function that takes in an argument called browser of type 'a (generic) and returns a string. You would need to change click myButtonFunc browser to click (myButtonFunc browser) browser for it to work but that is probably not what you are wanting.

Can you help me understand the purpose of myButtonFunc? Maybe I can help you achieve your goal.

jamesaslett1985 commented 3 years ago

Thanks @lefthandedgoat, I'm in the process of knocking up a repo so you can fire it up - will probably make more sense that way. I'll be back once I've sorted it!

lefthandedgoat commented 3 years ago

@jamesaslett1985 If you are wanting to create aliased functions so you dont have to pass in the browser every time you can use the other parallel option, the instanced one: https://lefthandedgoat.github.io/canopy//Api_Reference/canopy/canopy-parallell-instanced-instance.html

Its more c# style but should work also.

If thats not what you are wanting them I will look at your example once you get it posted.

jamesaslett1985 commented 3 years ago

Thanks @lefthandedgoat. Just to clarify, I'm creating a 'Page Object Model'-style framework, eg: I have a module per page of the website:

Module HomePage

let nextButton = "#nextBtn"

Because some of my let bindings are simply string selectors, I can perform canopy funcs on them at the test case level, which is quite nice, eg:

//Test case
click HomePage.nextButton
click SecondPage.button

In addition to this, I have funcs which will actually perform some canopy funcs, so I can call them nice and concisely like so:

Module HomePage

let forename applicant input = write $"#Applicant{applicant}_Forename" input
let lastname applicant input = write $"#Applicant{applicant}_Lastname" input
let name applicant forenameInput lastnameInput =
        write (forename applicant) fornameInput
        write (lastname applicant) lastnameInput
//Test case - just call the name func instead of both forename and surname
HomePage.name "1" "myFirstName" "myLastName"

I think I'm right in saying that with parallel.functions I can no longer use canopy funcs at the test case level, because I always have to pass in browser, and you can't pass that for just a string, eg: nextButton above. So I'd have to have something like:

Module HomePage

let nextButton browser = click "#nextBtn" browser

And call it like:

HomePage.nextButton browser

But with parallel.instanced, I have to use canopy funcs at the test case level, like so:

Module HomePage

let nextButton = "#nextBtn"
x.click (HomePage.nextButton)

I think I'm getting confused as to whether functions or instanced would be the best fit for my framework. Presumably there's no real difference in either approach other than stylistically? As you can probably tell, I'm new to F# :) Just trying to work out a 'best' approach before I go full steam ahead on the entire framework! I hope this all makes sense, because I am not entirely sure that it does!

lefthandedgoat commented 3 years ago

I think you are understanding is pretty solid but let me rephrase it and that may help.

For canopy classic everything is static including the 1 instance of the browser etc. The way you have written helper functions is fine. The only change I would make is that for code like this:

let forename applicant input = write $"#Applicant{applicant}_Forename" input
let lastname applicant input = write $"#Applicant{applicant}_Lastname" input
let name applicant forenameInput lastnameInput =
        write (forename applicant) fornameInput
        write (lastname applicant) lastnameInput

I would change to something like

Module Register

let forename name = write name "foreNameSelector"
let lastname name = write name "lastNameSelector"
let fullName fname lname =
        forename fname
        lastname lname

// in test
Register.fullname "Chris" "Holt" 

For parallel functions you are correct, you have to pass in the browser to all of your helpers so it would change to this

Module Register

let forename name browser = write name "foreNameSelector" browser
let lastname name browser = write name "lastNameSelector" browser
let fullName fname lname browser =
        forename fname browser
        lastname lname browser

// in test
Register.fullname "Chris" "Holt" browser

For the instanced version I would switch your page from a static class to an instanced based class also and you pass in the canopy instance into the class

Module Register

// add ctor

let forename name = x.write name "foreNameSelector"
let lastname name = x.write name "lastNameSelector"
let fullName fname lname =
        forename fname
        lastname lname

// in test 
let x = new canopy
let register = new Register(x)
register.fullname "Chris" "Holt"

All that being said there is a different way to run in parallel. You can keep your code in the static canopy classic style which has the cleanest syntax, tag your tests for different workflows or parts of the system, and just run your nunit.exe multiple times and merge the results. Its been years since I had done this but using Jenkins, it had a plugin that would run jobs in parallel and merge the results into the 'meta' job and it would pass if all sub jobs passed. This let us run our tests in parallel across multiple machines without doing any heavy lifting in the actual tests.

Last note: You know how global variables are frowned on very heavily and why? When your tests are running in parallel, your database is a massive set of global variables and you have to take great care in how you write your tests and have them create new users etc so that your tests dont walk on each other and randomly fail when they misalign. Its can be very painful.

Hope this helps, best of luck on your journey!

jamesaslett1985 commented 3 years ago

Thanks man, that's all very useful info! I'll pour over it properly tonight. One last question if I may - does the fact that some funcs in Parallel. Functions, eg: contains don't take a browser cause any issues, or will those work just fine without it?

lefthandedgoat commented 3 years ago

It will work fine without browser because its more inline with a normal unit test assertion, and not specifically related to browser based testing:

https://github.com/lefthandedgoat/canopy/blob/master/src/canopy/canopy.parallell.functions.fs#L545-L547

jamesaslett1985 commented 3 years ago

Absolutely amazing, thank you @lefthandedgoat :)