Recently I was looking for some code for sending emails via SMTP in C#. Below are few links which I found with some reusable code. Overall it looks fine , but have to include multiple validations for error handling .

In Nutshell, flow is as below

  • Define a function to send email which accepts an input Email object
  • Validate the email object to ensure all mandatory fields are present and correct
  • Create a new MailMessage object and SMTPClient Object and send email

Above gist links have some reusable code to achieve step 3 of above.

Recently one of my colleague approached me asking to help on creating a utility tool using selenium web driver. The requirement was simple which includes accepting few arguments from the command line and then open a browser and complete some actions on browser based on inputs provided. Having worked on selenium web driver for a few years, I thought this is relatively simple and can be done quickly.

It is implemented as a C# console app which had reference to selenium web driver. It accepts few arguments from command line and based on the values it opens up chrome browser and completes the action. The initial version was already there on which I made some modifications. We gave a demo to the user and thought it is all done.

As in any normal software projects, it was far from over. There were additional requirements to support multiple browsers, flexibility to provide arguments in any required order, the requirement to display detailed help text so that end user will know how to use the utility tool. All of them was done and we shared the build output, which included the exe file, all dlls used, drivers for various browsers and configuration files. That’s when I had my next requirement to make it as a portable EXE with the single exe file. That is not something which I had done before. Hence I spent quite some time to google and read through various approaches.

Fody.Costura

Costura is an addin for Fody. It helps to embed all assembly references into the output assembly/exe. Details documentation and source code can be found in github. Usage was pretty easy.

  • Install nuget package Install-Package Costura.Fody.
  • Create a FodyWeavers.xml ( modify if it exists) in the root folder of project . Update contents as below
1
2
3
4
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <Costura/>
</Weavers>
  • Make sure all required dlls are marked as “Copy Local”.
  • Build the project

This helped to combine all dlls like webdriver, newtonsoft dll etc into the utility exe file. The build output had only an exe file, config and other resources which I marked to copy to output.

Embedding Resources

Fody.Costura helped to combine exe files with required dlls and there by reducing number of files which needs to be distributed. However, I still had few text and json files which are resources for this tool. Initially, all required resources were copied to output and were accessed from there.

There is an option to embed all required files. It is done by changing BuildAction in properties to Embedded Resources. This will include files in output assembly which can be accessed in code.

This webpage has more details about how it can be done.

Below code snippet will show how it can be accessed. This shows how to read a resource file called Help.Txt

 var assembly = Assembly.GetExecutingAssembly();
 var resourceName = "NameSpaceName.SubFolderPathWhereResourceIsKept.Help.txt";
 using (Stream stream = assembly.GetManifestResourceStream(resourceName))
 using (StreamReader sr = new StreamReader(stream))
{
 var line = sr.ReadToEnd();
 Console.WriteLine(line);
}

After completing above two steps, I was able to combine all dlls and other required files into the Utility Exe. Now I just had to distribute exe file and the drivers for various browsers.

I recently faced an issue where one utility tool created by me was not running properly on another machine which had different dot net version installed. During troubleshooting, I was looking for ways to identify the installed dotnet version. Most of the links in google suggested to look for release value in registry as specified here.

Below powershell script will list down installed dotnet version on a machine. This is based on dotnet version listed on https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed. We may have to update below snippet as when new versions are released. Currently it supports upto dotnet 4.7.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$netRegKey = Get-Childitem "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full"  
$release = $netRegKey.GetValue("Release")  
Write-host $release
$releases =@(  
    @{id="378389";value=".NET Framework 4.5"},  
    @{id="378675";value=".NET Framework 4.5.1"},  
    @{id="379893";value=".NET Framework 4.5.2"},  
    @{id="393295";value=".NET Framework 4.6"},   
    @{id="393297";value=".NET Framework 4.6"},  
    @{id="394254";value=".NET Framework 4.6.1"}, 
    @{id="394271";value=".NET Framework 4.6.1"},  
    @{id="394802";value=".NET Framework 4.6.2"},      
    @{id="394806";value=".NET Framework 4.6.2"},  
    @{id="460798";value=".NET Framework 4.7"}, 
    @{id="460805";value=".NET Framework 4.7"},    
    @{id="461308";value=".NET Framework 4.7.1"}, 
    @{id="461310";value=".NET Framework 4.7.1"},
    @{id="461808";value=".NET Framework 4.7.2"},  
    @{id="461814";value=".NET Framework 4.7.2"}
    # Update more if new versions are released
)  
  
  
foreach($framework in $releases)  
{  
    if($framework.id -eq $release){  
        Write-Output $framework.value  
    }  
}  

Note: This assumes user can run Powershell with admin access so that it does not go through constraint language Mode . If running above script result in error like “Method Invocation is supported only on core types in this language mode”, it means it is on constraint language mode. In that case, we can run just Get-Childitem "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" and manually look for release key in above microsoft link

I was hosting my blog on github pages for past one year. Last week I decided to move hosting of my blog to AWS S3. There are obvious advantages of hosting a static site on S3. Moreover the cost of hosting is also minimal. There are many blogs in internet which explains the steps for hosting an octopress blog on S3.

Since this is my first exposure to AWS world, I did had a learning curve to get this done. Below are highlevel steps involved in hosting in S3.

  • Create an AWS login . Free plan was enough for my sites usage and traffic.
  • AWS recommends creating an IAM user for all activities instead of using root login. Hence I created an IAM user and gave permission to work on S3, Cloudfront, codecommit. Grab the AWS access KeyId and Secret Key from MyAccount > Security Credentials > Access Keys (for IAM user).
  • Install and configure s3cmd for uploading to S3 bucket. s3cmd is a free commandline tool to manage upload and retrieval of data from S3 bucket. Below are steps on Mac using homebrew for installing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Install Homebrew 
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# Install s3cmd
brew install s3cmd

# Verify installation
s3cmd --version

# Configure s3cmd
s3cmd --configure

# Configuration will require below information
* Access Key (use the access key from previous steps)
* Secret Key

  • Create S3 Bucket through s3cmd. Tutorial suggest creating S3 bucket with same domain name as static site.
1
2
# Create new S3 Bucket
s3cmd mb s3://www.my-new-bucket-name.com
  • Login to AWS management console and verify the bucket exist.
  • Navigate to properties and enable Static Website Hosting . Select index.html as index document. This will give the direct link to blog once it is hosted.
  • Also enable public access to read objects in Permissions tab.
  • Now, we need to modify the rake files to deploy to S3 Bucket. I was previously using Github pages and deploy was deploying to github. Hence made below changes to Rake File to deploy to S3 Bucket . It can be done by adding below details to Rake File.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#Modify Rake File with below details - Change Default_deploy and add new variable

#deploy_default = "push" ( This is existing value. Hence commenting it)
deploy_default = "s3"
s3_bucket = "www.my-new-bucket-name.com"
# Replace the bucket name in above line with actual name

#Add below at end of rake file

desc "Deploy website via s3cmd"
task :s3 do
  puts "## Deploying website via s3cmd"
  ok_failed system("s3cmd sync --acl-public --reduced-redundancy --skip-existing --cf-invalidate public/* s3://#{s3_bucket}/")
end

#skip-existing allows to upload only changed files. This will help to reduce the contents pushed across to S3 and will help to reduce cost
# However I still need to test out how the deleted files are refelected
# Cf-Invalidate will help to push the latest changes to cloudfront

  • Run rake deploy to deploy the website to AWS S3 bucket.
  • Configure domain DNS to point to the AWS site( the link generated while enabling static website hosting) . We need to configure corresponding DNS entires in domain provider(in my case CrazyDomain).
  • At this point we can even use AWS Cloud Front. I followed the steps mentioned here and here for setting up a cloudfront and corresponding SSL certificates. Process is straight forward and easy to follow. Only place I struggled is while configuring dns entries in crazy doman as part of dns validation step during SSL certificate generation. I could not find the place to enter NAME for CNAME field. It is named as Subdomain in crazydomain.
  • Finally setup CNAME in domain provider to redirect to cloudfront url for our blog

While playing around with AWS, I noticed that AWS codecommit is always free for normal user ( eventhough usage limitation apply ) . I found that usage limitation is pretty high and I may not have to worry about that. Hence I decided to use AWS code commit for keeping blog’s source repository . (Partly because Github doesn’t support private repo on free plan). Process was straight forward as we do with any other source control system like Bitbucket, gitlab or github. Only catch I found was , we need to seperately create Https Credentials/SSH keys for AWS codecommit in IAM. The user name and password is different from normal login. Once everything was setup, I just changed the remote repository details on my local and pushed it through.

Recently I had to find a way for running a command line process in server. I had to spend fair bit of time googling for various approaches of doing it. Most of them are by using PSExec. However there is another approach of using WMI (Windows Management Instrumentation) . Below is one of the approach , which I found at msdn blog.

Below method can be accessed anywhere by

1
2
ProcessWMI p = new ProcessWMI();
p.ExecuteRemoteProcessWMI(remoteMachine, sBatFile, timeout);

The solution has multiple parts as follows

  1. Connect to remote machine using remote machine Name, user name and password
  2. Start the remote process. Win32 process and pass the command to be run
  3. Find if the remote process is running and if it does, start an event monitor to wait for it to exit
  4. Once the process exits, retrieve its exit code

Code below is taken from above MSDN link ( Just to make sure it is available for me even if original MSDN link is unavailable in future.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class ProcessWMI
{
    public uint ProcessId;
    public int ExitCode;
    public bool EventArrived;
    public ManualResetEvent mre = new ManualResetEvent(false);
    public void ProcessStoptEventArrived(object sender, EventArrivedEventArgs e)
    {
        if ((uint)e.NewEvent.Properties["ProcessId"].Value == ProcessId)
        {
            Console.WriteLine("Process: {0}, Stopped with Code: {1}", (int)(uint)e.NewEvent.Properties["ProcessId"].Value, (int)(uint)e.NewEvent.Properties["ExitStatus"].Value);
            ExitCode = (int)(uint)e.NewEvent.Properties["ExitStatus"].Value;
            EventArrived = true;
            mre.Set();
        }
    }
    public ProcessWMI()
    {
        this.ProcessId = 0;
        ExitCode = -1;
        EventArrived = false;
    }
    public void ExecuteRemoteProcessWMI(string remoteComputerName, string arguments, int WaitTimePerCommand)
    {
        string strUserName = string.Empty;
        try
        {
            ConnectionOptions connOptions = new ConnectionOptions();
            //Note: This will connect  using below credentials. If not provided, it will be based on logged in user
             connOptions.Username = ConfigurationManager.AppSettings["RemoteMachineLogonUser"];
             connOptions.Password = ConfigurationManager.AppSettings["RemoteMachineUserPassword"];

            connOptions.Impersonation = ImpersonationLevel.Impersonate;
            connOptions.EnablePrivileges = true;
            ManagementScope manScope = new ManagementScope(String.Format(@"\\{0}\ROOT\CIMV2", remoteComputerName), connOptions);

            try
            {
                manScope.Connect();
            }
            catch (Exception e)
            {
                throw new Exception("Management Connect to remote machine " + remoteComputerName + " as user " + strUserName + " failed with the following error " + e.Message);
            }
            ObjectGetOptions objectGetOptions = new ObjectGetOptions();
            ManagementPath managementPath = new ManagementPath("Win32_Process");
            using (ManagementClass processClass = new ManagementClass(manScope, managementPath, objectGetOptions))
            {
                using (ManagementBaseObject inParams = processClass.GetMethodParameters("Create"))
                {
                    inParams["CommandLine"] = arguments;
                    using (ManagementBaseObject outParams = processClass.InvokeMethod("Create", inParams, null))
                    {

                        if ((uint)outParams["returnValue"] != 0)
                        {
                            throw new Exception("Error while starting process " + arguments + " creation returned an exit code of " + outParams["returnValue"] + ". It was launched as " + strUserName + " on " + remoteComputerName);
                        }
                        this.ProcessId = (uint)outParams["processId"];
                    }
                }
            }

            SelectQuery CheckProcess = new SelectQuery("Select * from Win32_Process Where ProcessId = " + ProcessId);
            using (ManagementObjectSearcher ProcessSearcher = new ManagementObjectSearcher(manScope, CheckProcess))
            {
                using (ManagementObjectCollection MoC = ProcessSearcher.Get())
                {
                    if (MoC.Count == 0)
                    {
                        throw new Exception("ERROR AS WARNING: Process " + arguments + " terminated before it could be tracked on " + remoteComputerName);
                    }
                }
            }

            WqlEventQuery q = new WqlEventQuery("Win32_ProcessStopTrace");
            using (ManagementEventWatcher w = new ManagementEventWatcher(manScope, q))
            {
                w.EventArrived += new EventArrivedEventHandler(this.ProcessStoptEventArrived);
                w.Start();
                if (!mre.WaitOne(WaitTimePerCommand,false))
                {
                    w.Stop();
                    this.EventArrived = false;
                }
                else
                    w.Stop();
            }
            if (!this.EventArrived)
            {
                SelectQuery sq = new SelectQuery("Select * from Win32_Process Where ProcessId = " + ProcessId);
                using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(manScope, sq))
                {
                    foreach (ManagementObject queryObj in searcher.Get())
                    {
                        queryObj.InvokeMethod("Terminate", null);
                        queryObj.Dispose();
                        throw new Exception("Process " + arguments + " timed out and was killed on " + remoteComputerName);
                    }
                }
            }
            else
            {
                if (this.ExitCode != 0)
                    throw new Exception("Process " + arguments + "exited with exit code " + this.ExitCode + " on " + remoteComputerName + " run as " + strUserName);
                else
                    Console.WriteLine("process exited with Exit code 0");
            }

        }
        catch (Exception e)
        {
            throw new Exception(string.Format("Execute process failed Machinename {0}, ProcessName {1}, RunAs {2}, Error is {3}, Stack trace {4}", remoteComputerName, arguments, strUserName, e.Message, e.StackTrace), e);
        }
    }
}

Code snippet for running power shell on a remote machine. Loosely based on blog post here and here

Code below is based on the sample code given in above two links

Add reference to System.Management.Automation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System.Management.Automation;
using System.Management.Automation.Runspaces;


  internal void runPowershellRemotely(string location, string scriptToBeRun)
        {
            string userName = ConfigurationManager.AppSettings["RemoteMachineLogonUser"];
            string password = ConfigurationManager.AppSettings["RemoteMachineUserPassword"];
           var securestring = new SecureString();
            foreach (Char c in password){
                securestring.AppendChar(c);
            }

            PSCredential creds = new PSCredential(userName, securestring);
            // Remove logging if not needed
            log.Info(String.Format("\tPOWERSHEL : Running Powershell {0} at location {1}", scriptToBeRun, location));
            WSManConnectionInfo connectionInfo = new WSManConnectionInfo();

           connectionInfo.ComputerName = ConfigurationManager.AppSettings["RemoteMachine"];
            connectionInfo.Credential = creds;
            Runspace runspace = RunspaceFactory.CreateRunspace(connectionInfo);
            runspace.Open();
            using (PowerShell ps = PowerShell.Create())
            {
                ps.Runspace = runspace;
                ps.AddScript(@"cd "+ location);
                ps.AddScript(scriptToBeRun);
                try
                {
                    var results = ps.Invoke();
                    log.Info("\tPOWERSHEL : Results from Powershell Script is ---------------------------");
                    foreach(var x in results)
                    {
                        log.Info(x.ToString());
                    }
                    log.Info("\tPOWERSHEL : End of results--------------------------------- ---------------------------");
                }
                catch (Exception e)
                {
                    log.Error("\tPOWERSHEL : Exception from running Powershell Script is" + e.ToString());
                }

            }
            runspace.Close();
        }

Below is code snippet for working with windows service. It helps to find status of service, start , stop and restart as required.

We need to pass in details of windows services name ( as shown i services.msc ) and machine name(should be in same network).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System.ServiceProcess;


          internal string FindStatus(string service, string server)
        {
            var myService = new ServiceController(service, server);
            log.Info(String.Format("\tStatus of {0} service in {1} is {2}", service, server, myService.Status.ToString()));
            return myService.Status.ToString();
        }

        internal string StopService(string service, string server)
        {
            var myService = new ServiceController(service, server);
            if (myService.Status == ServiceControllerStatus.Running)
            {
                myService.Stop();
                myService.WaitForStatus(ServiceControllerStatus.Stopped);
                log.Info(String.Format("\t{0} service Stopped in {1}. Current Status is {2}", service, server, myService.Status.ToString()));
            }
            return myService.Status.ToString();
        }

        internal string StartService(string service, string server)
        {
            var myService = new ServiceController(service, server);
            if (myService.Status == ServiceControllerStatus.Stopped)
            {
                myService.Start();
                myService.WaitForStatus(ServiceControllerStatus.Running);
                log.Info(String.Format("\t{0} service Started in {1}. Current Status is {2}", service, server, myService.Status.ToString()));
            }
            return myService.Status.ToString();
        }

Code snippet for comparing two xml files without using xsd for validating their structure is same ( nodes and arguments should be same. Values of each node/argument can be different).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
internal void VerifyMessageHaveSimilarStructureOfTemplate(string inputXml, string templateXml)
        {
            var docA = new XmlDocument();
            var docB = new XmlDocument();

            docA.LoadXml(inputXml);
            docB.LoadXml(templateXml);

            var isDifferent = DoTheyHaveDiferentStructure(docA.ChildNodes, docB.ChildNodes);
            log.Info("Result of Checking for difference of Input xml with template is : " + isDifferent.ToString());


        }


  private bool DoTheyHaveDiferentStructure(XmlNodeList xmlNodeListA, XmlNodeList xmlNodeListB)
        {
            if (xmlNodeListA.Count != xmlNodeListB.Count) return true;

            for (var i = 0; i < xmlNodeListA.Count; i++)
            {
                var nodeA = xmlNodeListA[i];
                var nodeB = xmlNodeListB[i];

                if (nodeA.Attributes == null)
                {
                    if (nodeB.Attributes != null)
                        return true;
                    else
                        continue;
                }

                if (nodeA.Attributes.Count != nodeB.Attributes.Count || nodeA.Name != nodeB.Name) return true;
                List<string> AttributeNameA = new List<string>();
                List<string> AttributeNameB = new List<string>();
                for (var j = 0; j < nodeA.Attributes.Count; j++)
                {
                    AttributeNameA.Add(nodeA.Attributes[j].Name);
                    AttributeNameB.Add(nodeB.Attributes[j].Name);
                    // -- If attribute position should be same, then include below as well
                    //var attrA = nodeA.Attributes[j];
                    //var attrB = nodeB.Attributes[j];

                    //if (attrA.Name != attrB.Name) return true;
                }
                AttributeNameA.Sort();
                AttributeNameB.Sort();
                if(! AttributeNameA.SequenceEqual(AttributeNameB)) return true;

                if (nodeA.HasChildNodes && nodeB.HasChildNodes)
                {
                    return HaveDiferentStructure(nodeA.ChildNodes, nodeB.ChildNodes);
                }
                else
                {
                    return true;
                }
            }
            return false;
        }

Cypress is not just UI automation tool . It can be used for testing APIs as well . Even though we have other tools like Postman, Newman, Rest Assured, SOAP UI etc for testing APIs, I believe cypress is a good alternative for testing API. It will help to use same tool for both UI and API test automation.

Demo

Let us look at a sample API test case. In below example, we trigger a API call to http://services.groupkt.com/country/get/iso2code/AU and validate below in the response.

  • Status code of response is 200.
  • Header include ‘application/json’.
  • Body contain “Country found matching code [AU].”

We can then extend this to do any further checks if needed.

Create a new file inside Integration folder of cypress and copy below code into that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
describe('API Testing with Cypress', () => {
    var result
    
    it('Validate the header', () => {
       result = cy.request('http://services.groupkt.com/country/get/iso2code/AU')
      
       result.its('headers')
             .its('content-type')
             .should('include', 'application/json')
        
    })

    it('Validate the status', () => {
        result = cy.request('http://services.groupkt.com/country/get/iso2code/AU')
       
        result.its('status')
              .should('equal',200);
     })

     it('Validate the body ', () => {
        result = cy.request('http://services.groupkt.com/country/get/iso2code/AU')
       
        result.its('body')
              .its('RestResponse.messages')
              .should('include', 'Country found matching code [AU].');
      
     })
}) 

Open Cypress by running node_modules/.bin/cypress open inside cypress root folder. This will open up Cypress.

Run newly created test.

APITestingWithCypress

Results of test execution will look like below.

[APITestingWithCypress]

Expand each of them and right click on the asserts and inspect the element. This will open up chrome developer tool. Select the console tab , which will list down details of calls made, request received and assertions performed. It will help to write additional assertions, investigate any failure etc.

[APITestingWithCypress]

When we talk about UI automation for browsers, the default tool which comes to mind is Selenium. There are different wrappers around selenium like protractor, Nightwatch , selenium webdriver etc. All of them are build on top of selenium and have all advantages /disadvantages of selenium. All of the control browser by executing remote commands through Network. We will most probably need additional libraries, framework etc to make full use of selenium.

Cypress.io is an open source UI automation tool which can be used for UI testing . Unlike others, this is not build on top of selenium . Instead is a complete new architecture and run in same run loop as browser. So it is running inside browser and have access to almost everything happening inside and outside browser. It is a complete set of tools that you will require to create and run E2E UI automation test cases. Team who developed cypress has made few design trade off which causes some disadvantages to cypress. There is no right tool for automation . It will depend on multiple factors.

Installing Cypress

We can install cypress using npm. Run below command inside project folder to install cypress and all dependencies.

1
npm install cypress --save-dev

Another way of using cypress is to download zip file from here . Just extract the file and start using it.

Opening Cypress

Cypress can be opened by running node_modules/.bin/cypress open command in terminal under /cypress.

If you have downloaded the zip file, you can open cypress by double clicking on the cypress executable.

Write your first test

Cypress already come with predefined example of KitchenSink application which will help you to identify various commands which can be used. It can be found under cypress\integration\example_spec.js.

Let us look at how to write a new test .

Create a new test script file called demotest.js under {project_location}\cypress\integration. Open up the file and write below code into it.

This code will open browser, load google and search for cypress.io and open up the first link.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe('My first test for cypress', function() {
    it('Visits google home page ', function() {
      cy.visit('https://google.com');
    })
    it('should load the Google Homepage', () => {
        cy.title().should('eql', 'Google');
    })
    it('should search and open cypress home page', () => {
        cy.get('#lst-ib').type('cypress.io');
        cy.get('[value="I\'m Feeling Lucky"]').focus().click();
    })
   
   
  })

Note: If cross origin policy error is shown, flow the workarounds mentioned.

How does cypress.io compare with Selenium

As mentioned earlier, there is no right or wrong tool for automation. It all depends on suitability for the task on hand. Let us compare few features where cypress.io and selenium have differences.

  • Cross browser support - At this point selenium have more cross browser support that cypress. Cypress supports only chrome variants. You can read about them here
  • Debugging capability - This is high in cypress. I found that error message are more details and infact provide some more details about how to fix it. Also you have full access to chrome dev tools.
  • Keypress - As of now , cypress doesnt support pressing Tab key . You can read about it here.
  • Since cypress is build on node.js, we can chain commands together
  • Cypress.io have built in support for test framework and assertion libraries like mocha, chai etc
  • Cypress.io test cases can be written in Javascript
  • Cypress.io handles wait times better than selenium
  • Cypress.io have hot reloading of test cases. When we make changes to test cases and save it , the test rerun by itself. This is very effective to reduce time spend on building and rerunning selenium based test cases.
  • Cypress.io have built in time travel and screenshots which will help us to go back to failure points and debug. It also capture before and after state for all actions

I will keep adding to this when I play more with cypress.