Multiple databases with Spring Boot MongoDB Repositories

Recently I needed to setup my Spring-Boot app to manage 2 instances of the Mongo driver. This allowed me to configure each one with database specific settings like the ReadPreference example below.

My requirements:

  1. I wanted all Spring configuration in Java code/annotations. (I hate XML!)
  2. I wanted Spring to manage both the Mongo and MongoTemplate objects.
  3. I wanted to use MongoRepository Interface beans and have the correct MongoTemplate back it.

The first thing we need to do is create a base configuration that will contain the generic beans shared between our two databases.

MongoConfiguration.java

@Configuration
public class MongoConfiguration {
    /**
     * For a clustered Mongo environment we would want to load multiple 
     * hosts. This will work if we use a single host or clustered.
     *
     * If the mongo.hosts key could not be found defaults to localhost
     *
     * One of the following would exist in our Spring properties file
     * 1) mongo.hosts = localhost
     * 2) mongo.hosts = 10.1.1.1,10.1.1.2,10.1.1.3
     */
    @Value("#{'${mongo.hosts:localhost}'.split(',')}")
    private List<String> hosts;

    /**
     * The port our Mongo hosts are running on.
     * Defaults to 27017
     */
    @Value("${mongo.port:27017}")
    private int port;

    /**
     * Creates a base Mongo instance that can be configured for each 
     * implementation.
     *
     * NOTE: If you are trying to connect to multiple MongoDB's then 
     * you would want to create 2 instances of this method as beans 
     * loading the correct mongo hosts. For my implementation I just 
     * wanted different global configurations pointed at the same 
     * database.
     *
     * @return A generic Mongo instance pointed at the hosts.
     * @throws Exception
     */
    private Mongo createMongo() throws Exception {
        final List<ServerAddress> serverList = new ArrayList<>();
        for (final String host : hosts) {
            serverList.add(new ServerAddress(host, port));
        }
        
        // MongoClientOptions would be created here and passed into 
        // the MongoClient as it's second param.
        return new MongoClient(serverList);
    }
    
    @Primary
    @Bean
    public Mongo readFromSecondaryNodeMongo() {
        final Mongo mongo = createMongo();
        // Do custom global configuration
        mongo.setReadPreference(ReadPreference.secondaryPreferred());
        return mongo;
    }
    
    @Bean
    public Mongo readFromPrimaryNodeMongo() {
        final Mongo mongo = createMongo();
        mongo.setReadPreference(ReadPreference.primaryPreferred());
        return mongo;
    }

    /**
     * This is the default DB Factory and will have the 
     * readFromSecondaryNodeMongo() bean injected due to the @Primary 
     * annotation
     * 
     * @param mongo auto injected using the @Primary bean
     * @return a new MongoDbFactory
     */
    @Bean
    public MongoDbFactory mongoDbFactory(Mongo mongo) {
        return new SimpleMongoDbFactory(mongo, "DatabaseName");
    }
}

Now that we have beans defined we need to create the configurations that will define and load the MongoTemplates into the correct repository interfaces.

ReadPrimaryNodeConfiguration.java

/**
 * The basePackages needs to point to the Interfaces
 * this configuration is going to back.
 */
@Configuration
@EnableMongoRepositories(
        basePackages = "com.example.primary",
        mongoTemplateRef = "readPrimaryNodeTemplate"
)
@Import(MongoConfiguration.class)
public class ReadPrimaryNodeConfiguration {
    /**
     * The @Qualifier here so we can load the non-default
     * Mongo bean.
     * 
     * @param mongo the non-default Mongo instance
     * @return a new MongoTemplate
     * @throws Exception thrown from MongoTemplate
     */
    @Bean
    public MongoTemplate readPrimaryNodeTemplate(
            @Qualifier("readFromPrimaryNodeMongo") Mongo mongo
    ) throws Exception {
        final MongoDbFactory factory = 
                new SimpleMongoDbFactory(mongo, "DatabaseName");
        return new MongoTemplate(factory);
    }
}

ReadSecondaryNodeConfiguration.java

/**
 * The basePackages needs to point to the Interfaces
 * this configuration is going to back.
 */
@Configuration
@EnableMongoRepositories(
        basePackages = "com.example.secondary",
        mongoTemplateRef = "readSecondaryNodeTemplate"
)
@Import(MongoConfiguration.class)
public class ReadSecondaryNodeConfiguration {
    /**
     * We don't use an @Qualifier here so the default 
     * MongoDbFactory in MongoConfiguration will be injected
     * 
     * @param factory the default factory
     * @return a new MongoTemplate
     * @throws Exception thrown from MongoTemplate
     */
    @Primary
    @Bean
    public MongoTemplate readSecondaryNodeTemplate(
            MongoDbFactory factory) throws Exception {
        return new MongoTemplate(factory);
    }
}

With that all we need to do is create the repository interface beans in the correct base package.

This will be backed by the MongoTemplate created in the ReadPrimaryNodeConfiguration.java

com.example.primary.ReadPrimaryNodeRepository.java

@Repository
public interface ReadPrimaryNodeRepository 
        extends MongoRepository<MyDocument, String> {
    // Method to look up a document by its ID
    MyDocument findById(String id);
}

And this one will be backed by the MongoTemplate created in ReadSecondaryNodeConfiguration.java

com.example.secondary.ReadSecondaryNodeRepository.java

@Repository
public interface ReadSecondaryNodeRepository 
        extends MongoRepository<MyDocument, String> {
    // Method to look up a document by its ID
    MyDocument findById(String id);
}

Test downloads with Neustar

Recently I was asked to write a Neustar script to download files from the browser. The goal of the script would be to calculate the sites performance during [X] downloads.

The following is the download step:

test.beginStep("Download");
var wrapper = driver.findElement(By.className("download-wrapper"));
// Find the name, size, and url of the download
var downloadName = wrapper.findElement(By.cssSelector(".download-info")).getText();
var downloadSize = wrapper.findElement(By.cssSelector(".size")).getText();
var downloadUrl = wrapper.findElement(By.partialLinkText("DOWNLOAD")).getAttribute("href");

// Fix name and size 
// Name: [EpicDownload.zip\n] (remove ' \n')
// Size: [99999999 bytes] (remove ' bytes') 
downloadName = downloadName.substr(0, downloadName.indexOf("\n"));
downloadSize = downloadSize.substr(0, downloadSize.indexOf(" "));

// Create the httpClient with the cookies from Selenium
var client = driver.getHttpClient();
client.setFollowRedirects(true);
client.setCheckValidSSL(false); // Test sites usually have invalid certs
client.setSocketOperationTimeout(60000); // This value will change depending on the site requirements

// If your site requires cookie authentication (Jsessionid) then we need to add the cookies
// to the httpRequest from Selenium.
var javaCookies = driver.manage().getCookies();

// Create the http GET request on the download url
var httpGet = client.newGet(downloadUrl);

// Call to custom setCookies function to add all the cookies to the http request
var httpRequest = setCookies(javaCookies, httpGet);

//Download the file to memory (do not write to the file system)
test.log("Starting download: " + downloadName + " [" + downloadSize + " bytes]");

// This will download the file from the server into memory
var httpResponse = httpRequest.execute();

// For our test we didn't need to assert anything but you could check that the download size is
// correct. Or the file name matches your expected.
var transactionStepObject = httpResponse.getInfo();
test.log("Download completed [" + transactionStepObject.getBytes() + " bytes] in " +
    transactionStepObject.getTimeActive() + "ms");
test.endStep();

//--- Util Functions

// Takes an array of javaCookies (from Selenium) and adds them to the httpRequest
var setCookies = function setCookies(javaCookies, httpRequest) {
    var javaCookiesArray = javaCookies.toArray();
    var cookieString = "";

    // Populate the cookieString with all cookies currently contained in the driver
    for (var i = 0; i < javaCookiesArray.length; i++) {
        var cookie = javaCookiesArray[i];
        cookieString += cookie.getName() + "=" + cookie.getValue() + ", domain=" + cookie.getDomain() + ", path=" +
            cookie.getPath() + "; ";
    }

    // Add the cookies as a request header
    httpRequest.addRequestHeader("Cookie", cookieString);

    return httpRequest;
};

Access the By class created using @FindBy annotation in Selenium

Recently I needed to increase the information the NoSuchElementException contained when using Selenium’s @FindBy annotation. During this I stumbled across a way to get access to the By class created from the @FindBy.

For example say we have the following PageObject


public class testPageObject {
   @FindBy(css = "h1[name='test']")
   WebElement testElement1;

   @FindBy(xpath = "//*[class='test']")
   WebElement testElement2;

   @FindBy(id = "test")
   WebElement testElement3;


   public testPageObject() {
       //Cache the WebElements using a 5 second wait
       PageFactory.initElements(new AjaxElementLocatorFactory(driver, 5), this);
   }

   public void printBy() {
        By testElement1 = getBy("testElement1");
        By testElement2 = getBy("testElement2");
        By testElement3 = getBy("testElement3");

        System.out.println(testElement1);
        System.out.println(testElement2);
        System.out.println(testElement3);
   }

   private By getBy(String fieldName) {
       try {
            return new Annotations(this.getClass().getDeclaredField(fieldName)).buildBy();
       } catch (NoSuchFieldException e) { return null; }
   }
}

The solution is to use Selenium’s built in Annotations class which takes a field and call buildBy() to return the By class for that field.

The output of the printBy() function:

By.selector: h1[name=’test’]
By.xpath: //*[class=’test’]
By.id: test

MyBatis with Annotation’s

The MyBatis configuration xml defines the connection info to the database and the mapper interfaces available to the database.

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="TestDB">

        <environment id="TestDB">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/"/>
                <property name="username" value="TestDBUser"/>
                <property name="password" value="TestDBPW"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper class="com.test.mapper.UserMapper"/>
        <mapper class="com.test.mapper.CompanyMapper"/>
    </mappers>

</configuration>

The Mapper class is an interface that contains the queries and sets the return type.

public interface UserMapper {

    final String ALL_USERS = "SELECT * FROM USER";
    final String USER_BY_SCREEN_NAME = ALL_USERS + " WHERE USER.screenname=#{screenName}";
    
    @Select(ALL_USERS)
    @Results(value = {
            @Result(property = "firstName",     column = "firstname"),
            @Result(property = "lastName",      column = "lastname"),
            @Result(property = "userName",      column = "screenname"),
            @Result(property = "emailAddress",  column = "emailaddress")
    })
    List<User> getUsers();

    @Select(USER_BY_SCREEN_NAME)
    @Results(value = {
            @Result(property = "firstName",     column = "firstname"),
            @Result(property = "lastName",      column = "lastname"),
            @Result(property = "userName",      column = "screenname"),
            @Result(property = "emailAddress",  column = "emailaddress")
    })
    User getUserByScreenName(String screenName);
}

To use the new mapper I usually will create a DatabaseBuilder class that uses generics to return the mapper for that database.

public class DatabaseBuilder {
    private final String conf;

    public DatabaseHelper(Database database) {
        this.conf = database.getEnvironment();
    }

    public <T> T getMapper(Class<T> mapperInterface) {
        try {
            Reader reader = Resources.getResourceAsReader(conf);

            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sessionFactory = builder.build(reader);
            SqlSession session = sessionFactory.openSession();

            return session.getMapper(mapperInterface);
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("Failed to return mapper");
    }

    public <T> T getMapper(Class<T> mapperInterface, String environment) {
        try {
            Reader reader = Resources.getResourceAsReader(conf);

            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sessionFactory = builder.build(reader, environment);
            SqlSession session = sessionFactory.openSession();

            return session.getMapper(mapperInterface);
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("Failed to return mapper");
    }

    public <T> T getMapper(Class<T> mapperInterface, Properties properties) {
        try {
            Reader reader = Resources.getResourceAsReader(conf);

            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sessionFactory = builder.build(reader, properties);
            SqlSession session = sessionFactory.openSession();

            return session.getMapper(mapperInterface);
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("Failed to return mapper");
    }

    public <T> T getMapper(Class<T> mapperInterface, String environment, Properties properties) {
        try {
            Reader reader = Resources.getResourceAsReader(conf);

            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sessionFactory = builder.build(reader, environment, properties);
            SqlSession session = sessionFactory.openSession();

            return session.getMapper(mapperInterface);
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("Failed to return mapper");
    }

    public enum Database {
        //The path to your MyBatis xml file
        TESTDB("test-db.conf.xml")
        // If you have more then one DB with custom mappers place them here!
        //TESTDB2("test_db2.conf.xml")

        private String config;

        private Database(String config) {
            this.config = config;
        }

        public String getEnvironment() {
            return this.config;
        }
    }
}

Now you can use your custom queries with the following

UserMapper userMapper = new DatabaseBuilder(Database.TESTDB).getMapper(UserMapper.class);
List<User> users = userMapper.getUsers();
User testUser = userMapper.getUserByScreenName("EpicTester");

Updated @DataProvider on @Factory contained within test class

Just wanted to post an update on using TestNG’s @Factory with @DataProvider.

A few gotcha’s you may encounter:
1) When placing your @DataProvider and @Factory inside the test class you cannot place @Test on the class itself. In the following example doing so will result in: org.testng.TestNGException:
Method createExampleClass requires 3 parameters but 0 were supplied in the @Test annotation. 

2) Make sure to create your @Factory and @DataProvider methods as static

ExampleClass:

public class ExampleClass {
    @DataProvider(name = "data")
    public static Iterator<Object[]> getData() {
        List<Object[]> data = new ArrayList<Object[]>();

        //Our array types must match the @Factory input (String, Integer, Boolean)
        data.add(new Object[]{"Object 1", 1, true});
        data.add(new Object[]{"Object 2", 2, false});

        return data.iterator();
    }

    @Factory(dataProvider = "data")
    public static Object[] createExampleClass(String s, Integer i, Boolean b) {
        //Instantiate the @Test class supplying the constructor our values
        return new Object[]{new ExampleClass(s, i, b)};
    }

    private String s;
    private Integer i;
    private Boolean b;

    //Created with values from @DataProvider in @Factory
    public ExampleClass(String s, Integer i, Boolean b) {
        this.s = s;
        this.i = i;
        this.b = b;
    }

    @Test
    public void unitTest() {
        System.out.println(s + ", " + i + ", " + b);
    }
}

Another “preferred” way to use the constructor as the factory:

public class ExampleClass {
    @DataProvider(name = "data")
    public static Iterator<Object[]> getData() {
        List<Object[]> data = new ArrayList<Object[]>();

        //Our array types must match the @Factory input (String, Integer, Boolean)
        data.add(new Object[]{"Object 1", 1, true});
        data.add(new Object[]{"Object 2", 2, false});

        return data.iterator();
    }

    private String s;
    private Integer i;
    private Boolean b;

    //Created with values from @DataProvider
    @Factory(dataProvider = "data")
    public ExampleClass(String s, Integer i, Boolean b) {
        this.s = s;
        this.i = i;
        this.b = b;
    }

    @Test
    public void unitTest() {
        System.out.println(s + ", " + i + ", " + b);
    }
}

TestNG Lazy DataProvider with Factory

Last week I ran into an issue passing a lazy dataprovider to a factory. The following is the issue I first ran into:

The issue I was having is that when I ran my class it ran X (# of .xml files) number of tests using the last value contained in the array.

So if I had [“Oregon.xml”, “NewYork.xml”, “Washingtion.xml”] Washington will run 3 times.
class supplyData{

    @DataProvider
    public static Iterator<Object[]> loadStatePages(){

        List<Object[]> allParsedXMLPages = new ArrayList<Object[]>()
        int loadNextState = 0

        //create array containing x "StatePage.xml"; where x = # of states
        def allStatePages = getCityStatePages("State")

        allStatePages.each {singleStatePage ->
            def loadedStatePage = loadFile(singleStatePage)

            //create array containing [0]STATENAME, [2]ABBREVNAME, [3]ARRAY CITYNAMES
            def allStates = getStateContainer()

            //load next state to load into currentState
            def currentState = allStates[loadNextState]

            def fullStateName = currentState[0].toString()
            def abbrevState = currentState[1].toString()

            //create allCities by remove the fullStateName and abbrevState from array
            def allCities = currentState
            allCities.remove(0)
            allCities.remove(0)

            def cityLink = ""

            for (city in allCities) {
                def cityHref = city.text().replaceAll(/\W+/, '-')
                cityLink = cityLink +('<link>\n' +
                        '<title>'+ city + '</title>\n' +
                        '<linktext>' + city + '</linktext>\n' +
                        '<asserthref>/' + cityHref.toLowerCase() + '</asserthref>\n' +
                        '<asserttitle>' + city + ", " + fullStateName + '</asserttitle>\n' +
                        '</link>\n'
                )
            }

            String bodyStatePage = loadedStatePage.getText()

            ++loadNextState

            def parsedStatePage = parseString(bodyStatePage)

            def completeStatePage = createFullXMLPage(bodyStatePage, parsedStatePage)

            //add the parsed page to our ArrayList
            allParsedXMLPages.add(completeStatePage as Object[])
        }

        return allParsedXMLPages.iterator()
    }
}
public class CityTests{
    static cityCurrentXML

    public CityTests(newXML){
        this.cityCurrentXML = newXML
    }

    @Factory (dataProviderClass=supplyData.class, dataProvider="loadCityPages")
    public static Object[] getNextXML(cityCurrentXML){
        return [new CityTests(cityCurrentXML)]
    }

    @BeforeClass
    void openBrowser(){
        startBrowser()
    }

    @Test (description="Check MetaTags for correct content")
    public void MetaTags(){
        def errors = testMetaTags(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Links for existance, clickability, and target")
    public void Links(){
        def errors = testLinks(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Text for existance and accuracy")
    public void Text(){
        def errors = testText(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Overlays for existance, links, and text")
    public void Overlays(){
        def errors = testOverlays(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Hovers for existance, links, and text")
    public void Hovers(){
        def errors = testHovers(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }
    @Test (description="Check Inputs for existance, typeability, and target")
    public void Inputs(){
        def errors = testInputs(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Sliders for existance, links, and text")
    public void Sliders(){
        def errors = testSliders(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Images for existance and display")
    public void Images(){
        def errors = testImgLoaded(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check AtlasTags for existance and value")
    public void AtlasTags(){
        def errors = testAtlas(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @AfterClass
    void tearDown(){
        driver.quit()
    }
}
To fix this issue I had to make the following changes:
class supplyData{

class supplyData{

    @DataProvider
    public static Object[][] loadStatePages(){

        List<Object[]> allParsedXMLPages = new ArrayList<Object[]>()
        int loadNextState = 0

        //create array containing x "StatePage.xml"; where x = # of states
        def allStatePages = getCityStatePages("State")

        allStatePages.each {singleStatePage ->
            def loadedStatePage = loadFile(singleStatePage)

            //create array containing [0]STATENAME, [2]ABBREVNAME, [3]ARRAY CITYNAMES
            def allStates = getStateContainer()

            //load next state to load into currentState
            def currentState = allStates[loadNextState]

            def fullStateName = currentState[0].toString()
            def abbrevState = currentState[1].toString()

            //create allCities by remove the fullStateName and abbrevState from array
            def allCities = currentState
            allCities.remove(0)
            allCities.remove(0)

            def cityLink = ""

            for (city in allCities) {
                def cityHref = city.text().replaceAll(/\W+/, '-')
                cityLink = cityLink +('<link>\n' +
                        '<title>'+ city + '</title>\n' +
                        '<linktext>' + city + '</linktext>\n' +
                        '<asserthref>/' + cityHref.toLowerCase() + '</asserthref>\n' +
                        '<asserttitle>' + city + ", " + fullStateName + '</asserttitle>\n' +
                        '</link>\n'
                )
            }

            String bodyStatePage = loadedStatePage.getText()

            ++loadNextState

            def parsedStatePage = parseString(bodyStatePage)

            def completeStatePage = createFullXMLPage(bodyStatePage, parsedStatePage)

            //add the parsed page to our ArrayList
            allParsedXMLPages.add(completeStatePage as Object)
        }

        return allParsedXMLPages
    }
}
public class CityTests{
    private cityCurrentXML

    public CityTests(newXML){
        this.cityCurrentXML = newXML
    }

    @Factory (dataProviderClass=supplyData.class, dataProvider="loadCityPages")
    public static Object[] getNextXML(cityCurrentXML){
        return [new CityTests(cityCurrentXML)]
    }

    @BeforeClass
    void openBrowser(){
        startBrowser()
    }

    @Test (description="Check MetaTags for correct content")
    public void MetaTags(){
        def errors = testMetaTags(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Links for existance, clickability, and target")
    public void Links(){
        def errors = testLinks(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Text for existance and accuracy")
    public void Text(){
        def errors = testText(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Overlays for existance, links, and text")
    public void Overlays(){
        def errors = testOverlays(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Hovers for existance, links, and text")
    public void Hovers(){
        def errors = testHovers(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }
    @Test (description="Check Inputs for existance, typeability, and target")
    public void Inputs(){
        def errors = testInputs(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Sliders for existance, links, and text")
    public void Sliders(){
        def errors = testSliders(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check Images for existance and display")
    public void Images(){
        def errors = testImgLoaded(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @Test (description="Check AtlasTags for existance and value")
    public void AtlasTags(){
        def errors = testAtlas(cityCurrentXML)

        assertPageTest(errors, cityCurrentXML.PAGENAME.text())
    }

    @AfterClass
    void tearDown(){
        driver.quit()
    }
}
As you can see I removed the lazy dataProvider as I was unable to pass the object correctly to the factory. The main issue I had was when passing the object to a static variable the factory recreate the class with the last loaded static value. Hope this helps you get a dataProvider with factory working correctly for you!
Justin

Intro to web automation with Selenium WebDriver and TestNG inside IntelliJ IDEA

After many painstaking weeks of crawling wiki’s, documentation, and websites trying to figuring out how selenium and testng work together. I decided to write up a tutorial that covers some of the basics.

Selenium Webdriver:

When you first start working with Selenium it can be pretty confusing. The first thing you’ll want to familiarize yourself with is the 3 different versions, Selenium IDE (a Firefox plugin), Selenium Remote Control (Selenium 1 or Selenium RC), and Selenium WebDriver (Selenium 2 or just webdriver). The Selenium website has tons of documentation covering what Selenium is and isn’t, and what each version does. For the purpose of this tutorial we will be working with Selenium WebDriver aka Selenium 2. You have the option of downloading either the SeleniumServer.jar or SeleniumClient.jar from the downloads page. Choose which one best suits your needs.

TestNG:

Next we’ll go over TestNG. Shamelessly stolen from their site, “TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use.” If you’ve never worked with a unit testing framework before this may sound Greek to you. Basically TestNG allows you to add annotations before your classes and methods. This allows you create classes and methods as tests, dataProviders which pass data to our tests, BeforeClass, BeforeTest, BeforeMethod to setup tests, or AfterClass, AfterTest, AfterMethod to tear down our tests! If this doesn’t make much sense right now don’t worry we will cover it later on.

IntelliJ IDEA

The last thing you’ll need to get is a Java IDE. In this tutorial I’m going to be using IntelliJ’s IDEA. If your more comfortable with Eclipse or any other IDE feel free to follow along.

Getting Started: Set up

The first thing you’ll want to do is create a new project from scratch in IDEA

Name your project and make sure ‘Java Module’ is selected then click Next

Leave ‘Create source directory’ checked and click Next

Your now given the option to use Groovy code. NOTE: I usually use Groovy as it’s quicker to code. However if your plan is to compute complex calculations, Groovy becomes very sluggish in this area.

Select if your going to use Groovy and click Finish

Now that you have a project to work with right click on your ‘src’ file and create a new Groovy or Java class.

Name your new class and click OK

Now that we have our first class we need to add the webdriver and testng .jar files to our project. To do that click File -> Project Structure

Inside the Project Structure dialog choose Libraries under Project Settings and click the + icon then Java

Navigate to the location of your selenium or testng .jar file, select it, and click OK

Click OK in the following Choose Modules window

Repeat to add 2nd .jar file to project

Your Libraries should now contain both selenium and testng

Congrats! You are now ready to start building automation using Selenium WebDriver and TestNG.

To continue Read Part 2