Sunday, 26 February 2017

This chapter is from The Agile Tester V4 which was released Feb 2017 

 A Complete C# example

The Java version is here - 

http://seleniumj.blogspot.co.uk/ 


So let us now look at a complete worked example using Visual Studio, C#, Selenium and the ExtentReports add-on.
For this example I will assume you have already installed Visual Studio and Selenium.
However you may not have the ExtentReports add-on installed.

To install this add-on simply load the Package Manager Console and type in the following:
Install-Package ExtentReports -Version 2.40.2
Visual Studio will then install the package.

This example also uses MySql, so if you also need this add-on then install with this command:
Install-Package MySql.Data -Version 6.9.9

You should also note that these version numbers will change over time.

Okay so when you're ready below is the code for this example.
First we have the project layout.




So the first thing you will notice here is there are four class files, now let's start with the main class file, in this case I am referring to spiceTest.cs

The full code for this class is shown next.





using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Selenium
{
    [TestClass()]
    public sealed class spiceTest
    {
       
        private static spiceClass sc;

        [AssemblyInitialize()]
        public static void AssemblyInit(TestContext context)
        {
            //nothing to do
        }

        [ClassInitialize()]
        public static void ClassInit(TestContext context)
        {
            sc = new spiceClass();
            sc.beforeClass(1);
        }

        [TestInitialize()]
        public void Initialize()
        {
            sc.logFail("Test initialize - Nothing to do");
        }


        /*
         *Test Case 1 - check the site under test actually loads
         * */
        [TestMethod()]
        public void Test1()
        {
            try {
                Assert.IsTrue(sc.loadWebSite(1, "",1));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 2 - This test will attempt to load and display a specific recipe
        * */
        [TestMethod()]
        public void Test2()
        {
            try
            {
                Assert.IsTrue(sc.loadWebSite(1, "recipe.aspx?ndx=160",2));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 3 - This test will try to search on a empty search box
        *Test should return all records
        * */
        [TestMethod()]
        public void Test3()
        {
            try
            {
                Assert.IsTrue(sc.searchWebSite(1, "search.aspx", "", "selection.aspx?ndx=SRCH", true,3));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 4 - This test will try to search on %shark%
        *Test should return more than 1 record
        * */
        [TestMethod()]
        public void Test4()
        {
            try
            {
                Assert.IsTrue(sc.searchWebSite(1, "search.aspx", "shark", "selection.aspx?ndx=SRCHshark", true,4));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 5 - This test will try to search on %kevin%
        *Test should return 0 record
        * User should be returned to search page
        * */
        [TestMethod()]
        public void Test5()
        {
            try
            {
                Assert.IsTrue(sc.searchWebSite(1, "search.aspx", "kevin", "search.aspx", false,5));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }


        /*
        *Test Case 6 - This test will try and load the list of chili recipes
        * User should be shown list page
        * */
        [TestMethod()]
        public void Test6()
        {
            try
            {
                Assert.IsTrue(sc.loadMenuItem(1, "", "//*[@id='form1']/div[5]/main/ul[1]/li[1]/ul/li[1]/a", "selection.aspx?ndx=CH",6));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }


        /*
        *Test Case 7 - This test will try and load the list of curry recipes types
        * User should be shown list page
        * */
        [TestMethod()]
        public void Test7()
        {
            try
            {
                Assert.IsTrue(sc.loadMenuItem(1, "", "//*[@id='form1']/div[5]/main/ul[1]/li[1]/ul/li[2]/a", "selection.aspx?ndx=IN",7));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 8 - This test will try and load the list of curry chicken recipes
        * User should be shown list page
        * */
        [TestMethod()]
        public void Test8()
        {
            try
            {
                Assert.IsTrue(sc.loadMenuItem(1, "selection.aspx?ndx=IN", "//*[@id='body_center_narrow']/table/tbody/tr[3]/td/a", "selection.aspx?ndx=IN&ct=3",8));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 9 - This test will try and load the list of curry fish recipes
        * User should be shown list page
        * */
        [TestMethod()]
        public void Test9()
        {
            try
            {
                Assert.IsTrue(sc.loadMenuItem(1, "selection.aspx?ndx=IN", "//*[@id='body_center_narrow']/table/tbody/tr[7]/td/a", "selection.aspx?ndx=IN&ct=7",9));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        *Test Case 10 - This test will try to load a recipe
        * User should be shown the recipe page
        * */
        [TestMethod()]
        public void Test10()
        {
            try
            {
                Assert.IsTrue(sc.loadMenuItem(1, "selection.aspx?ndx=IN&ct=7", "//*[@id='body_center_narrow']/table/tbody/tr[7]/td/a", "recipe.aspx?ndx=153",10));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }


        /*
        *Test Case 11 - This test will try to print a recipe
        * User should be shown the recipe print page
        * */
        [TestMethod()]
        public void Test11()
        {
            try
            {
                Assert.IsTrue(sc.printMenuItem(1, "recipe.aspx?ndx=153", "printRecipe.aspx","//*[@id='main']/table/tbody/tr[1]/td/a","recipe.aspx?ndx=153",11));
            }  
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
        * Test Case 12-19
        * Add new recipe
        * Tests all required fields
        * */
        [TestMethod()]
        public void Test12()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 0, "submit.aspx",12));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        [TestMethod()]
        public void Test13()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 1, "submit.aspx",13));
            }  
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }

        [TestMethod()]
        public void Test14()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 2, "submit.aspx",14));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }

        [TestMethod()]
        public void Test15()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 3, "submit.aspx",15));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }


        [TestMethod()]
        public void Test16()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 4, "submit.aspx",16));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }

        [TestMethod()]
        public void Test17()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 5, "submit.aspx",17));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        [TestMethod()]
        public void Test18()
        {
            try
            {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 6, "submit.aspx",18));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }


        [TestMethod()]
        public void Test19()
        {
            try {
                Assert.IsTrue(sc.AddRecipe(1, "submit.aspx", 7, "submit.aspx",19));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }


        /*
         * Test Case 20
         * Try to access the admin area
         * without logging in
         * */
        [TestMethod()]
        public void Test20()
        {
            try
            {
                Assert.IsTrue(sc.tryLogin(1, "adminList.aspx", "default.aspx", 20, false,"", ""));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }

        }

        /*
         * Test Case 21
         * Try to access the wine admin area
         * without logging in
         * should return false
         * */
        [TestMethod()]
        public void Test21()
        {
            try
            {
                Assert.IsTrue(sc.tryLogin(1, "adminWineList.aspx", "default.aspx", 21, false, "", ""));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
         * Test Case 22
         * Try to access recipe no 10
         * without logging in
         * should return false
         * */
        [TestMethod()]
        public void Test22()
        {
            try
            {
                Assert.IsTrue(sc.tryLogin(1, "adminRecipe.aspx?ndx=8", "default.aspx", 22, false, "", ""));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }


        /*
         * Test Case 23
         * Try to login to the admin area
         *
         * */
        [TestMethod()]
        public void Test23()
        {
            try
            {
                Assert.IsTrue(sc.tryLogin(1, "adminLogin.aspx", "adminList.aspx", 23, true, "", ""));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }


        /*
        * Test Case 24
        * Try to access recipe no 10
        * after logging in
        * */
        [TestMethod()]
        public void Test24()
        {
            try {
            Assert.IsTrue(sc.tryLogin(1, "adminLogin.aspx", "adminRecipe.aspx?ndx=10", 24, true, "//*[@id='body_center_narrow']/table/tbody/tr[11]/td[1]/a",""));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
         * Test Case 25
         * Try to access wine no 10
         * after logging in
         * */
        [TestMethod()]
        public void Test25()
        {
            try
            {
                Assert.IsTrue(sc.tryLogin(1, "adminLogin.aspx", "adminWine.aspx?ndx=10", 25, true, "//*[@id='body_center_narrow']/table/tbody/tr[11]/td[1]/a", "wineBtn"));
            }
            catch (Exception e)
            {
                sc.logFail(e.Message);
            }
        }

        /*
         * Test cleanup, runs after every test case
         * */
        [TestCleanup()]
        public void Cleanup()
        {
            sc.logFail("Test cleanup - Nothing to do");
        }

        /*
         * Class cleanup, runs after the complete test run
         * */

        [ClassCleanup()]
        public static void ClassCleanup()
        {
            sc.afterClass();
            sc = null;
        }

        [AssemblyCleanup()]
        public static void AssemblyCleanup()
        {
            //nothing to do
        }
    }
}



So now let's have a look at this class. As you can see there is a reference to spiceClass (private static spiceClass sc;). Here is where the actual tests take place.

When you run these tests the first thing to be called with be the static function AssemblyInitialize. This function can be used before all tests in the assembly have run and to allocate resources obtained by the assembly. In this example the function is not required.
Next ClassInitialize is called, here anything that needs to be created or initialized prior to the test run should be processed.

Finally before the first test is executed you have TestInitialize. This function is run before each test, in this example it is not required.

So that now moves us on to the tests themselves. In this example we have twenty five test cases. These make up a complete regression test of my recipe website (www.spicetheword.com). Each test runs in alphabetical order, therefore test1 will run before test2 and test2 will run before test3 etc.
Each test case has a similar style as per this example:



[TestMethod()]
public void Test4()
{
   try
   {
      Assert.IsTrue(sc.searchWebSite(1, "search.aspx", "shark", "selection.aspx?ndx=SRCHshark", true,4));
   }
   catch (Exception e)
   {
     sc.logFail(e.Message);
   }
}

Each test case calls a function in the referenced spiceClass class. In every example, if the test runs correctly a true value will be returned hence the Assert.IsTrue. Any variation of this is caught in the catch and the error is logged in the output report.

Assert.IsTrue is only one possible option here, there are many others and below is just a small selection.
·         Assert.IsFalse
·         Assert.IsNull
·         Assert.Equals

So each test case is run in turn and after every case TestCleanup is called, in this example it is not used. When all test cases are complete the ClassCleanup is called and this is a good place to close down all open files, streams etc. Finally AssemblyCleanup is called.

That is the main class but every test case calls a function in spiceClass.cs so let us now look at that class.




using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using RelevantCodes.ExtentReports;

namespace Selenium
{
    class spiceClass : baseClass
    {
        Boolean passed = false;

        public void logFail(String err)
        {
            printIt(err);
        }

        /*
         * beforeClass
         * setup a few things before starting the test run
         *
         * */
        public void beforeClass(int Browser)
        {
            passCnt = 0;
            failCnt = 0;
            baseUrl = "http://spicetheworld.com/";
            report = new ExtentReports(reports + "spicetheworld.html", true);
            report.AddSystemInfo("Test Server", baseUrl);
            report.AddSystemInfo("Testing Dude", "Kev Martin");
            report.AddSystemInfo("QA Manager", "Kev Martin");
            test = report.StartTest("SpiceTheWorld.com - Full regression");
            test.AssignCategory("Regression");
            printIt("Set up the test parameters");
            if (Browser == 1) driver = new ChromeDriver(@"Chrome");
            driver.Manage().Window.Maximize();
        }


        public void afterClass()
        {
            int tCnt = passCnt + failCnt;
            printIt("Total tests completed : " + tCnt);
            printIt("Total tests passed    : " + passCnt);
            printIt("Total tests failed    : " + failCnt);
            printIt("Close the browser");
            closeDownBrowser();
            printIt("End the test");
            report.EndTest(test);
            report.Flush();
            report.Close();
        }

        public Boolean loadWebSite(int Browser, string url, int tc)
        {
            printIt("Test case " + tc + ". Load page " + url);
            setBrowser(Browser, url);
            if (driver.Url.Equals(baseUrl + url))
            {
                printAndShootIt("Test case " + tc + ". Load page " + baseUrl + url + " passed","tc"+tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Load page " + url + " failed", "tc" + tc);
                passed = false;
                failCnt++;
            }
            return passed;
        }

        public Boolean searchWebSite(int Browser, string url, string sString, string retPath, Boolean isFnd, int tc)
        {
            setBrowser(Browser, url);
            driver.FindElement(By.Id("searchTxt")).Clear();
            driver.FindElement(By.Id("searchTxt")).SendKeys(sString);
            driver.FindElement(By.Id("searchButton")).Click();
            if (driver.Url.Equals(baseUrl + retPath))
            {
                printAndShootIt("Test case " + tc + ". Search on " + sString + " at " + baseUrl + url + " passed", "tc" + tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Search on " + sString + " at " + baseUrl + url + " failed", "tc" + tc);
                passed = false;
                failCnt++;
            }
            return passed;
        }

        public Boolean loadMenuItem(int Browser, string url, string xpath, string dest, int tc)
        {
            setBrowser(Browser, url);
            driver.FindElement(By.XPath(xpath)).Click();
            doSleep(1000);
            if (driver.Url.Equals(baseUrl + dest))
            {
                printAndShootIt("Test case " + tc + ". Load Menu Item at " + baseUrl + url + " passed", "tc" + tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Load Menu Item at " + baseUrl + url + " failed", "tc" + tc);
                passed = false;
                failCnt++;
            }
            return passed;
        }


        public Boolean printMenuItem(int Browser, string url, string prUrl, string xpath, string dest, int tc)
        {
            setBrowser(Browser, url);
            driver.Navigate().GoToUrl(baseUrl + prUrl);
            doSleep(2000);
           
            if (driver.Url.Equals(baseUrl + prUrl))
            {
                printAndShootIt("Test case " + tc + ". Print Menu Item at " + baseUrl + url + " passed", "tc" + tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Print Menu Item at " + baseUrl + url + " passed", "tc" + tc);
                passed = false;
                failCnt++;
            }
            return passed;
        }

        public Boolean tryLogin(int Browser, string url, string dest, int tc, Boolean tryLogin, string xpath, string wine)
        {
            if (tryLogin==true) getLogin();
            setBrowser(Browser, url);
            if (tryLogin == true)
            {
                driver.FindElement(By.Id("uid")).Clear();
                driver.FindElement(By.Id("uid")).SendKeys(loginStr);
                driver.FindElement(By.Id("pword")).Clear();
                driver.FindElement(By.Id("pword")).SendKeys(passwStr);
                driver.FindElement(By.Id("loginButton")).Click();
                doSleep(1000);
            }

            if (wine.Length != 0)
            {
                driver.FindElement(By.Id(wine)).Click();
                doSleep(1000);
            }

            if (xpath.Length!=0)
            {
                driver.FindElement(By.XPath(xpath)).Click();
                doSleep(1000);

            }

            if (driver.Url.Equals(baseUrl + dest))
            {
                printAndShootIt("Test case " + tc + ". Login to Admin Page", "tc" + tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Login to Admin Page", "tc" + tc);
                passed = false;
                failCnt++;
            }

            return passed;
        }


        public Boolean AddRecipe(int Browser, string url, int ndx, string dest, int tc)
        {
            setBrowser(Browser, url);
            driver.FindElement(By.Id("name")).Clear();
            driver.FindElement(By.Id("name")).SendKeys(rName[ndx]);
            driver.FindElement(By.Id("serves")).Clear();
            driver.FindElement(By.Id("serves")).SendKeys(rServings[ndx]);
            driver.FindElement(By.Id("yname")).Clear();
            driver.FindElement(By.Id("yname")).SendKeys(yName[ndx]);
            driver.FindElement(By.Id("ingred")).Clear();
            driver.FindElement(By.Id("ingred")).SendKeys(ingred[ndx]);
            driver.FindElement(By.Id("method")).Clear();
            driver.FindElement(By.Id("method")).SendKeys(method[ndx]);
            driver.FindElement(By.Id("ymail")).Clear();
            driver.FindElement(By.Id("ymail")).SendKeys(ymail[ndx]);

            driver.FindElement(By.Id("submitButton")).Click();
            doSleep(1000);
           
            if (driver.Url.Equals(baseUrl + dest))
            {
                printAndShootIt("Test case " + tc + ". Add recipe passed", "tc" + tc);
                passed = true;
                passCnt++;
            }
            else
            {
                printAndShootIt("Test case " + tc + ". Add recipe failed", "tc" + tc);
                passed = false;
                failCnt++;
            }
            return passed;
        }
    }
}





The first thing you will notice is that this class inherits from another class called baseClass as shown with this line:
class spiceClass : baseClass

A lot of the variables and functions used in spiceClass can be found in baseClass and we will look at this class later.

The first function you can see is the beforeClass function. This is called before any test cases are run and there this is where all of the pre-test setups will take place. This includes creating the ExtentReports output file and loading the test browser.
The next function is the afterClass. This is called when all of the test cases have been completed and is there where all closing up should be done. This will including closing the output file and the test browser.

Next, you will see the actual functions used by the test cases. These use now familiar commands such as FindElement and also additional functions you will find in the inherited class such as printAndShootIt(). So let us now look at baseClass.cs

using System;
using System.Threading;
using OpenQA.Selenium;
using RelevantCodes.ExtentReports;
using System.Drawing.Imaging;


namespace Selenium
{
    class baseClass: dbClass
    {

        /*
         * load the selected webapge into the test browser
         * url holds the required web page
         * */
        protected void setBrowser(int Browser, string url)
        {
            printIt("Load browser");
            printIt("Load web page " + baseUrl + url);
            driver.Navigate().GoToUrl(baseUrl + url);
            doSleep(2000);
        }

        /*
         * thread sleep functions, allows current thread to complete before
         * the script tries to move on
         * @param ms - milliseconds
         */
        protected void doSleep(int ms)
        {
            try
            {
                Thread.Sleep(ms);
            }
            catch (Exception e)
            {
                Thread.CurrentThread.Interrupt();
            }
        }


        /*
         * Close the test browser
         *
         * */
        protected void closeDownBrowser()
        {
            try
            {
                driver.Close();
                driver.Quit();
            }
            catch (Exception e)
            {
            }
        }

         /*
         * Create the initial output for the test report
         *
         * */
        protected void createHeader()
        {
            printIt("Browser name : Chrome");
            printIt("Starting SpiceTheWorld test suite on browser: Chrome ");

        }

        /*
         * Print the text and also create a screenshot
         *
         * */
        protected void printAndShootIt(String theText, String fileName)
        {
            printIt(theText);
            shootIt(fileName);
        }

        /*
        * Print the text
        *
        * */
        protected void printIt(String theText)
        {
            test.Log(LogStatus.Info, theText);
        }


        /*
         * Create a screenshot
         * name is value of string filename
         * */
        protected void shootIt(String fileName)
        {
            path = takeScreenShot(fileName);
            String imagePath = test.AddScreenCapture(path);
            test.Log(LogStatus.Info, "Screenshot", imagePath);
        }


        /*
        * Take the actual screenshot
        *
        * */
        public String takeScreenShot(String fileName)
        {
            fileName = fileName.Replace(" ", "");
            Screenshot ss = ((ITakesScreenshot)driver).GetScreenshot();
            string screenshot = ss.AsBase64EncodedString;
            byte[] screenshotAsByteArray = ss.AsByteArray;
            String path = images + fileName + ".png";
            ss.SaveAsFile(path, ImageFormat.Png);
            return path;
        }
    }
}
So these are useful functions for closing the browser, printing text to the report file and creating screenshots of the test. You will also notice this class also inherits the dbClass class. So let is now look at that class.
using System;
using OpenQA.Selenium;
using RelevantCodes.ExtentReports;
using MySql.Data.MySqlClient;

namespace Selenium
{
    class dbClass
    {
        protected string[] rName = { "", "Test Recipe", "Test Recipe", "Test Recipe", "Test Recipe", "Test Recipe", "Test Recipe", "Test Recipe" };
        protected string[] rServings = { "", "", "4-6", "4-6", "4-6", "4-6", "4-6", "4-6" };
        protected string[] yName = { "", "", "", "Fred Bloggs", "Fred Bloggs", "Fred Bloggs", "Fred Bloggs", "Fred Bloggs" };
        protected string[] ingred = { "", "", "", "", "Inred", "Inred", "Inred", "Inred" };
        protected string[] method = { "", "", "", "", "", "Method", "Method", "Method" };
        protected string[] ymail = { "", "", "", "", "", "", "fred", "kev@kevsbox.com" };

        protected IWebDriver driver = null;
        protected String baseUrl = "";
        protected ExtentReports report;
        protected ExtentTest test;
        protected String loginStr = "";
        protected String passwStr = "";
        protected String path = "";
        protected String images  = @"C:\Selenium\reports\images\";
        protected String reports = @"C:\Selenium\reports\";
        private String constr = "";
        private MySqlConnection con = null;

        protected int passCnt = 0;
        protected int failCnt = 0;
        protected Boolean passed = false;

        private void openCon()
        {
            try
            {
                con = new MySqlConnection(constr);
                con.Open();
            }
            catch (Exception e)
            {
                string err = e.Message;
            }
        }


        protected void getLogin()
        {
            openCon();
            String sqlString = "SELECT userid, password FROM spicetheworld.logins WHERE ndx=2";
            MySqlCommand cmd = new MySqlCommand(sqlString, con);
            MySqlDataReader rs = cmd.ExecuteReader();
            while (rs.Read())
            {
                loginStr = rs.GetString(0);
                passwStr = rs.GetString(1);
            }
            rs.Close();
            con.Close();
        }
    }
}
You will notice that this class does not inherit from anything else, so this is as deep as we go.

The functions in this class relate to connecting to a database and retrieving login information which is required in the tests that attempt to access the admin area of spicetheworld.com. For obvious reasons I have removed the login information from this book.

The other items in this class is a list of variables. The private variables are only visible in this class while the protected variables are visible in baseClass and spiceClass.

So that is it a full working example that I have used on www.spicetheworld.com. If you want to view the results report then you can see it here: www.kevsbox.com/spicetheworld.html











1 comment: