Web3 backend and smart contract development for Python developers Musical NFTs part 7: writing tests for smart contracts
Tags: python, blockchain, ethereum, nft
Published: 2023-09-26 15:01:44

Brownie relay on Pytest for smart contracts testing. For testing purpose we will need only two new files inside: `./tests` folder: `conftest.py` and `test_contracts.py`

`helpers.py` file have (we already create for deployment purpose) one function which returns eth acocunts from which we will deploy our contracts inisde our test.  

`conftest.py` is used to set-up initial conditions in which we can test some smart-contract functionality. For this purpose we will use `@pytest.fixtures` decorators. This will allow us to pass them as argument to our test and on this way to set-up intial conditions in which we can test our smart contract functionality. Here is code for `conftest.py` (more explanation in comments):

    #!/usr/bin/python3

    import pytest

    #function for loading account
    from scripts.helpers import get_account

    
    initial_supply = 1000
    max_nft = 100

    """
    Defining a Shared Initial State for this test. A common pattern in testing is to include one or more module-scoped setup fixtures that define the 
    initial test conditions, and then use fn_isolation (next step) to revert to this base state at the start of each test. 
    """


    @pytest.fixture(scope="module", autouse=True)
    def initial_supply_of_mock_tokens():
        # return intial_supplay of MockUSDC tokens
        return initial_supply



    @pytest.fixture(scope="module", autouse=True)
    def smartContract_deploy(MusicNFT, MockUSDC):
        account = get_account()
        # deploy MockUSDC contract
        mock_token = MockUSDC.deploy(initial_supply, {"from": account[0]})
        # deploy MusciNFT contract
        musicNFTtoken = MusicNFT.deploy(mock_token, 10, {"from": account[0]})
        # return depoyment addresses of both contract. What deploy method return is much more richer, but in this moment we use just address
        return mock_token, musicNFTtoken


    """
    In many cases we want to isolate our tests from one another by resetting the local environment. Without isolation, it is possible that the outcome of 
    a test will be dependent on actions performed in a previous test. This is done by following function. 
    """


    @pytest.fixture(scope="function", autouse=True)
    def isolate(fn_isolation):
        # perform a chain rewind after completing each test, to ensure proper isolation
        # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html #isolation-fixtures
        pass


    @pytest.fixture(scope="module")
    def test_accounts():
        account_1, account_2 = get_account()
        return account_1, account_2



What we miss now is just `test_contract.py` file. Here si more details

    #!/usr/bin/python3

    # Test if deployment whent well and if address pf deployed contracts starts with 0x
    def test_initial_supplay_account_alice(smartContract_deploy):
        mock_token, musicNFTtoken = smartContract_deploy
        assert mock_token.address.startswith("0x") 
        assert musicNFTtoken.address.startswith("0x") 


    # Test if right ammount of MockTokens are minted to deployer/owner address
    def test_mock_token(initial_supply_of_mock_tokens, smartContract_deploy, test_accounts):
        mock_token, musicNFTtoken =  smartContract_deploy
        deployer_account, user_account = test_accounts
        initial_supply = initial_supply_of_mock_tokens
        mock_token_balance = mock_token.balanceOf(deployer_account)
        assert mock_token_balance == 1000_000_000_000_000_000_000 


    # Test approve & allowance functionality. Reason for this is fact that when we have crypto 
    # buyers for NFTs we will need to transfer from our user to our smart contract certain 
    # amount of USDC tokens. And because we will call USDC contract from our MusicNFT contract 
    # we will need to have rights to spend user USDC by transfering from his account to 
    # our account. 
    def test_mock_token_approve_allow(smartContract_deploy, test_accounts):
        accountOne, accountTwo = test_accounts
        mock_token, musicNFTtoken =  smartContract_deploy
        mock_token.approve(musicNFTtoken.address, 1000)
        result = mock_token.allowance(accountOne, musicNFTtoken.address)
        assert result == 1000


    # And now finally let's test our NFT contract. And here in this test we will check whole buy with crypto flow. 
    # And this includes: buyer approve our NFT contract to spend USDC 
    # for NFT in this name. 
    def test_NFT_buy_with_crypto(smartContract_deploy, test_accounts):
        mock_token, musicNFTtoken =  smartContract_deploy
        deployer_account, user_account = test_accounts
        mock_token.approve(musicNFTtoken.address, 1000)
        result = mock_token.allowance(deployer_account, musicNFTtoken.address)
        assert result == 1000
        mock_token.transfer(musicNFTtoken.address, 1000)
        mock_token_balance = mock_token.balanceOf(musicNFTtoken.address)
        assert mock_token_balance == 1000



Let's run test

    $brownie test -v

    =============================================================================== 4 passed in 2.54s ================================================================================
    Terminating local RPC client...
    (env)  brownie_musical_nfts/tests git:(main) ✗ ➜ brownie test -v
    Brownie v1.19.3 - Python development framework for Ethereum

    ============================================================================== test session starts ===============================================================================
    platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /home/ilija/code/my_tutorials/musical_nft_thumbnails/smart-contracts/env/bin/python3
    cachedir: tests/.pytest_cache
    hypothesis profile 'brownie-verbose' -> verbosity=2, deadline=None, max_examples=50, stateful_step_count=10, report_multiple_bugs=False, database=DirectoryBasedExampleDatabase(PosixPath('/home/ilija/.brownie/hypothesis'))
    rootdir: /home/ilija/code/my_tutorials/musical_nft_thumbnails/smart-contracts/brownie_musical_nfts
    plugins: eth-brownie-1.19.3, forked-1.4.0, hypothesis-6.27.3, web3-5.31.3, xdist-1.34.0
    collected 4 items                                                                                                                                                                
    This version of µWS is not compatible with your Node.js build:

    Error: Cannot find module './uws_linux_x64_111.node'
    Falling back to a NodeJS implementation; performance may be degraded.



    Launching 'ganache-cli --chain.vmErrorsOnRPCResponse true --server.port 8545 --miner.blockGasLimit 12000000 --wallet.totalAccounts 20 --hardfork istanbul --wallet.mnemonic attitude grant adjust accuse mail visual hammer potato nest interest breeze crime --wallet.defaultBalance 200'...

    test_contracts.py::test_initial_supplay_account_alice PASSED                                                                                                               [ 25%]
    test_contracts.py::test_mock_token PASSED                                                                                                                                  [ 50%]
    test_contracts.py::test_mock_token_approve_allow PASSED                                                                                                                    [ 75%]
    test_contracts.py::test_NFT_buy_with_crypto PASSED                                                                                                                         [100%]

    =============================================================================== 4 passed in 2.41s ================================================================================


In this moment we should have our smart-ontract written, tested and deployed. Next step is integration with Django backend. 

Code can be found in github [repo](https://github.com/ilijapet/musical_nft_thumbnails)