Building a production-ready Ethereum DApp from idea to final product — The smart-contract (Part 3)

in #blockchain6 years ago

1*V61RihtAuGzwcFpSolI0Ag.png

In the previous post, we built the application structure for our solidity code. In this article, we are going to develop our smart-contract (business logic) for the Survey DApp.

Table of content

This series is separated into 3 sections:

  1. The smart-contract — Create the Survey smart-contract (You’re here)
    1.1. From Idea to a business plan
    1.2. Setting up development environment with Truffle
    1.3. Creating the smart-contract the TDD way  — You’re here
    1.4. Testing Solidity Code on TestRPC, Local Ethereum chain and Testnet
  2. The Backend — Server side Ethereum development
  3. The Frontend — Client side user interface and wallet integration

Incremental updates the TDD way

I’m a big fan when it comes to TDD and BDD, and one thing that TDD does best is to minimise the process of thinking about your code and approach the source code in a more functional approach.

BDD is using the same concepts as TDD but communicates it using examples instead; for example:

“Given that I’m a [ROLE]
When I [CONDITION]
Then [RESULT]”

In this article you will see the above format used for clarity purposes.

We can also create Solidity Mocks, but it will not allow certain test cases to be executed since we cannot control the msg.sender and the msg.value variables, so I will preferably work with Truffle’s Mocha test suits.

We are going to extend Mocha test suit and use it with ChaiJS to make a better quality code for my contract.

Creating the Contracts Shell

The first thing we need to create is the shell for our smart-contract, in this Simple DApp, we only need to create two solidity contracts.

  1. SurveyFactory.sol — The solidity contract that creates the survey to keep track of the survey’s owner and optionally charge fees from Survey Makers to run the service.

  2. Survey.sol — The solidity contract that works with the survey itself like adding participants, removing participants, finishing survey and more.

In the contracts folder, create two files SurveyFactory.sol and Survey.sol and write the minimal code for a valid solidity contract, such as:

pragma solidity ^0.4.19;
/// @title SurveyFactory — Creates surveys and charge users
/// @author {YOUR NAME HERE}— <{YOUR EMAIL HERE}>
contract SurveyFactory {
 //Just a shell...for now!
}

And do the exact same thing for Survey.sol as well.

pragma solidity ^0.4.19;
/// @title Survey — Work with a Survey to add participants finish survey 
/// @author {YOUR NAME HERE}— <{YOUR EMAIL HERE}>
contract Survey{
 //Just a shell...for now!
}

If you installed Truffle globally, you can create the contracts by simply running the command truffle create contract SurveyFactory and truffle create contract Survey. However, you will have to change the default solidity compiler version.

Creating the Test Suits

Now we need to create the same for our tests, we need to have two test suits, one for SurveyFactory and the other for Survey.

In the test folder, create two files survey_factory.js and survey.js and write the minimal code for a valid solidity contract, such as :

//Survey.js
contract('Survey', _accounts => {
  /* Initialization code here */

  beforeEach(async () => {
    /* Before Each Test here */
  });

  describe('[TEST_CASES_LOGICAL_GROUP_HERE]', () => {

    it(`1. Given that I'm the ____
        2. When I ____
        3. Then I should ____`, () => {
        /* Test Login here */
      });
    
  });
  
});
// survey_factory.js
contract('SurveyFactory', _accounts => {
  /* Initialization code here */

  beforeEach(async () => {
    /* Before Each Test here */
  });

  describe('[TEST_CASES_LOGICAL_GROUP_HERE]', () => {

    it(`1. Given that I'm the ____
        2. When I ____
        3. Then I should ____`, () => {
        /* Test Login here */
      });
    
  });
  
});

If you installed Truffle globally, you can create test files by simply running the command truffle create test SurveyFactory and truffle create test Survey. This will create the same identical files.

Preparing the test suits

Before we start with the test cases, we need to figure out how we want to work with specific common variables and patterns throughout your test cases; it is usually a good practice to separate those in helper classes and make yourself DRY from the beginning.

Error Helpers

The first helper is to work with Solidity errors. Since the most common exception is going to appear from a require statement, then we need to figure out a way in which we can detect that. Luckily Solidity throws the code “revert” to that error, and we can try to see if the error contains the “revert” keyword or it is a different error.

This helper is using the same concepts as the assertRevert.js from zeppelin-solidity library.

Create a folder inside the testdirectory named helpers and add the following file inside of it.

// expectRevert.js
// Inspired By:
// https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/assertRevert.js
const expect = require('chai').expect;
    
module.exports = async (promise) => {
    try {
        await promise;
        assert.fail('Expected revert not received');
    } catch (error) {
        expect(error.message, `Expected "revert", got ${error} instead`).to.contain('revert');
    }
};

Packages and Common Variables helper

Another essential helper is to group all the variables that we think is going to be utilised by our test cases together so that we DRY.

We first need to think about the Survey DApp roles; we have three roles in our DApp:

  1. Survey DApp Owner
  2. Survey Maker
  3. Participants

It makes sense to create variables for these accounts, also set other shared variables, for example, the Survey DApp Creation Fee or different constants.

Inside helpers directory, add the following file :

//common.js
/* Loading all imports */
const _ = require('lodash');
const expectRevert = require('./expectRevert');
const Survey = artifacts.require("./Survey.sol");
const SurveyFactory = artifacts.require("./SurveyFactory.sol");
const BigNumber = web3.BigNumber;

require('chai')
    .use(require('chai-bignumber')(BigNumber))
    .use(require('chai-as-promised'))
    .should();//To enable should chai style

/* Creating a class with all common Variables */
class CommonVariables {
    constructor(_accounts) {
        this.accounts = _accounts;

        this.appOwner = _accounts[0]
        this.surveyMaker = _accounts[1]
        this.participants = _.difference(_accounts, [_accounts[0], _accounts[1]]);

        this.surveyCreationCost = web3.toWei('1', 'ether'); //5 Stars services!
        this.surveyReward = web3.toWei('1', 'ether');
        this.surveyRewardAndCreationCost = web3.toWei('2', 'ether');
    }
}

/* Exporting the module */
module.exports = {
    BigNumber,
    SurveyFactory,
    Survey,
    expectRevert,
    CommonVariables
}

With that in mind, we can go back to our test files survey_factory.js and survey.js and use the helpers we created, it should looks something similar to the following:

//survey.js
/* Loading all libraries from common */
const {
  SurveyFactory, //Survey Factor Contract
  Survey, //Survey Contract
  BigNumber, //BigNumber from web3 (for ease to use)
  CommonVariables, //Multiple common variables
  expectRevert, //Check if the Solidity returns "revert" exception (usually result from require failed)
} = require('./helpers/common');


contract('Survey', _accounts => {
  const commonVars = new CommonVariables(_accounts);

  let accounts = commonVars.accounts;

  const _appOwner = commonVars.appOwner;
  const _surveyMaker = commonVars.surveyMaker;
  const _participants = commonVars.participants;

  const _surveyCreationCost = commonVars.surveyCreationCost;
  const _surveyReward = commonVars.surveyReward;
  const _surveyRewardAndCreationCost = commonVars.surveyRewardAndCreationCost;

  let surveyFactory = null;
  let survey = null;
  
  beforeEach(async () => {
    surveyFactory = await SurveyFactory.new(_surveyCreationCost, { from: _appOwner });
    // TODO - create a survey!
  });

  describe('[TEST_CASES_LOGICAL_GROUP_HERE]', () => {

    it(`1. Given that I'm the ____
        2. When I ____
        3. Then I should ____`, () => {
        /* Test Login here */
      });
  });

});
// survey_factory.js
/* Loading all libraries from common */
const {
  SurveyFactory, //Survey Factor Contract
  Survey, //Survey Contract
  BigNumber, //BigNumber from web3 (for ease to use)
  CommonVariables, //Multiple common variables
  expectRevert, //Check if the Solidity returns "revert" exception (usually result from require failed)
} = require('./helpers/common');


contract('SurveyFactory', _accounts => {
  /* Initialization code here */
  const commonVars = new CommonVariables(_accounts);

  let accounts = commonVars.accounts;

  const _appOwner = commonVars.appOwner;
  const _surveyMaker = commonVars.surveyMaker;
  const _participants = commonVars.participants;

  const _surveyCreationCost = commonVars.surveyCreationCost;
  const _surveyReward = commonVars.surveyReward;
  const _surveyRewardAndCreationCost = commonVars.surveyRewardAndCreationCost;

  let surveyFactory = null;

  beforeEach(async () => {
    surveyFactory = await SurveyFactory.new(_surveyCreationCost, { from: _appOwner });
  });

  describe('[TEST_CASES_LOGICAL_GROUP_HERE]', () => {

    it(`1. Given that I'm the ____
        2. When I ____
        3. Then I should ____`, () => {
        /* Test Login here */
      });

  });


});

Note that I did repeat some code twice (initialization code), that is for readability purposes, and ease of access!

Now, we first need to run ganache-cli, just execute npm run testrpc in a command-line and open a new command line to prepare for testing.

With the above, we are ready to work on our test cases and start coding some solidity.

Starting the TDD Cycle

1 - Adding Test Case(s)

Adding the test case(s) by creating a logical group using describe and add various test case examples underneath it, for example, we need to cover test cases for “Create Survey” functionality, so we can build our test cases to cover all examples of this requirements. Such as:

Example #1

  1. Given that I’m the Survey Maker
  2. When I try to create a new Survey and included the survey creation costs and survey reward
  3. Then I should be able to get the created survey reference number and address

Example #2

  1. Given that I’m the Survey Maker
  2. When I try to create a new Survey and included the survey creation costs and survey reward
  3. Then I should be assigned as the owner of the newly created survey

Example #3

  1. Given that I’m the Survey Maker
  2. When I try to create a new survey without including the survey creation cost.
  3. Then I should receive an Error

Example #4

  1. Given that I’m the Survey App Owner
  2. When I try to create a new Survey
  3. Then I should receive an Error

Below is the code that covers all four examples:

// survey_factory.js
describe('Create New Survey Test Cases', () => {

    // ## Example #1
    it(`1. Given that I’m the Survey Maker
        2. When I try to create a new Survey and included the survey creation costs and survey reward
        3. Then I should be able to get the created survey reference number and address`, () => {
        return surveyFactory.createSurvey.call({ value: _surveyRewardAndCreationCost, from: _surveyMaker })
          .then(([surveyId, surveyAddress]) => {
            return surveyId;
          }).should.eventually.be.bignumber.equals(0);
      });
  
    //  ## Example #2
    it(`1. Given that I’m the Survey Maker
        2. When I try to create a new Survey and included the survey creation costs and survey reward
        3. Then I should be assigned as the owner of the newly created survey`, () => {
        let newSurveyAddress;
        return surveyFactory.createSurvey.call({ value: _surveyRewardAndCreationCost, from: _surveyMaker })
          .then(([surveyId, surveyAddress]) => {
            newSurveyAddress = surveyAddress;
            return surveyFactory.createSurvey({ value: _surveyRewardAndCreationCost, from: _surveyMaker });
          }).then(receipt => {
            return Survey.at(newSurveyAddress);
          }).then(surveyInstance => {
            return surveyInstance.owner.call();
          }).should.eventually.equal(_surveyMaker);
      });

    //  ## Example #3
    it(`1. Given that I’m the Survey Maker
        2. When I try to create a new survey without including the survey creation cost
        3. Then I should receive a "revert" Error`, () => {

        return expectRevert(surveyFactory.createSurvey.call({ from: _surveyMaker }));

      });

    //  ## Example #4
    it(`1. Given that I’m the Survey App Owner
        2. When I try to create a new Survey
        3. Then I should receive a "revert" Error`, () => {

        return expectRevert(surveyFactory.createSurvey.call({ from: _appOwner }));

      });

});

Two things to note from the code above:

  1. I didn’t use async/await and used it only in the beforeEach override, this is just a style preference, I usually depend on using chai-as-promised library, but you can still use it as you please.

  2. In the second test case, I invoked the method createSurvey first with .call and then without it, the first one is meant to get the result and the second one executes the transaction to be mined by the Ethereum Blockchain (returns a transaction receipt).

2 - Update Solidity Code to add enough logic for the test to succeed

Now that we created the test cases we need to make sure the solidity code can make those test cases succeed.

I changed both Solidity contracts to make my test passes, the solidity code now looks like the following:

//Survey.sol
pragma solidity ^0.4.19;

/// @title Survey - A survey instant created by SurveyFactory to randomize the winning process of the fees
/// @author Amr Gawish
contract Survey {

    /* Events */
    event SurveyInitialized(address indexed owner, uint indexed surveyReward);


    /* Contract State */
    address public owner;
    address private factory;

    /// @notice Constructor of the Survey Contract
    /// @param _owner - The survey maker of this survey (needs to be a real address!)
    /// @dev Initialise the Survey with Survey Reward (must be > 0)
    function Survey(address _owner) payable public {
        require(owner != address(0));
        require(msg.value > 0);
        
        owner = _owner;
        factory = msg.sender;
        SurveyInitialized(owner, msg.value);
    }


}
//SurveyFactory.sol
pragma solidity ^0.4.19;

import "./Survey.sol";
import "zeppelin-solidity/contracts/ownership/Ownable.sol";
import "zeppelin-solidity/contracts/math/SafeMath.sol";

/// @title SurveyFactory - Creates surveys and charge users
/// @author Amr Gawish
contract SurveyFactory is Ownable{
    
    using SafeMath for uint;

    /* Events */
    event SurveyFactoryInitialized(uint indexed surveyCreationFees);
    event SurveyCreated(uint indexed suveryId, address indexed surveyAddress);


    /* Modifiers */
    modifier notTheOwner(){
        require(msg.sender != owner);
        _;
    }
    
    /* Contract State */
    uint public surveyCreationFees ;
    address[] public surveys ;
    mapping(uint => address) public surveyToOwner ;

    /// @notice Constructor of the Survey Factory Contract
    /// @param _surveyCreationFees - The fees to charge user when they create their survey
    /// @dev Initialise the Survey Factory with Survey Creation Fees value
    function SurveyFactory(uint _surveyCreationFees) public {
        surveyCreationFees = _surveyCreationFees;
        SurveyFactoryInitialized(surveyCreationFees);
    }


    /// @notice Create a new Survey instance
    /// @dev checks against the following:
    ///      1. msg.sender and revert if the sender is the Survey DApp Owner
    ///      2. msg.value to be bigger than surveyCreationFees (The difference is the Initialized Survey Reward)
    /// @return surveyId - The index os the Survey 
    /// @return newSurveyAddress - The address of the newly created contract
    function createSurvey() external payable notTheOwner returns(uint surveyId, address newSurveyAddress) {
        require(msg.value > surveyCreationFees);

        uint surveyReward = msg.value.sub(surveyCreationFees);

        address _newSurveyAddress = (new Survey).value(surveyReward)(msg.sender);
        uint _surveyId = surveys.push(_newSurveyAddress).sub(1);
        surveyToOwner[_surveyId] = msg.sender;
        

        SurveyCreated(_surveyId, _newSurveyAddress);
        return (_surveyId, _newSurveyAddress);
    }
}

Things to note here:

  1. I might have created more than what the test cases need to pass; this is because I have an architecture and I know specific variables/modifiers would be handy in the future.

  2. When you have public/external methods, make sure they emit an event, always!

  3. Always be paranoid and do your checks (usually using require) to make sure the method is not continuing with any corrupt state.

  4. Also use SafeMath for any arithmetic operations, always!

  5. If the contract has Ownership logic, import zeppelin Ownable contract!

3- Test & Repeat

Test the above code by running npm run test to make sure it passes all test cases.

Then repeat the above two steps for another requirement, until we cover all our cases, and our application is ready!

In the Survey DApp, there are four other significant functionalities to test like:

  1. Adding Participants
  2. Increasing the survey reward
  3. Finishing Survey and Draw Winner Randomly
  4. Invalidate Survey and pull money out

In the next part

With what we’ve covered in this post, we have successfully looked at how to build the main business logic for the application in a TDD/BDD fashion. We can now deploy the contract and tested it.

The Git repository doesn’t have all test cases covered yet; it would be great if you want to help include it by creating a PR and add the new test-cases and solidity code. However, the full source code with test cases should be available before the next blog post.


If you liked this article, consider upvoting it, and follow me for more.
Also, follow me on Twitter!

Sort:  

Congratulations @agawish! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

Click here to view your Board

Do not miss the last post from @steemitboard:

Carnival Challenge - Collect badge and win 5 STEEM
Vote for @Steemitboard as a witness and get one more award and increased upvotes!

Congratulations @agawish! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

Use your witness votes and get the Community Badge
Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.28
TRX 0.13
JST 0.032
BTC 66304.34
ETH 2983.64
USDT 1.00
SBD 3.68