Desenvolvimento

27 abr, 2017

Chaincode para desenvolvedores de Go – parte 03

Publicidade

Esta é a terceira parte do artigo Chaincode para desenvolvedores Go. Se você não leu os artigos anteriores, leia aqui a parte 01 e a parte 02.

Nas duas primeiras partes desta série, falamos dos fundamentos, como o papel do chaincode e das APIs para interagir com o Fabric subjacente, bem como tópicos avançados como modelagem de dados, controle de acesso e eventos.

A partir daqui, veremos os conceitos de TDD e como aplicar esta abordagem para escrever chaincode em Golang para Hyperledger Fabric v0.6.

Normalmente, testar a unidade do seu chaincode seria complicado, já que você precisaria primeiro implantar o chaincode em uma rede blockchain em um contêiner Docker para ter acesso à infraestrutura de blockchain subjacente, como ao ledger, às informações de transação, etc. Este tutorial apresenta uma abordagem alternativa, onde você pode facilmente testar a unidade do seu chaincode usando meu CustomMockStub, que estende o MockStub disponível no pacote shim.

Este tutorial também inclui exemplos que mostram como você pode acabar com uma função não determinística em seu chaincode e como testar essas funções não determinísticas.

Pré-requisitos

Vamos continuar com o caso de uso do aplicativo de empréstimo imobiliário apresentado na Parte 1 desta série de tutoriais.

  • Você deve estar familiarizado com os tenants básicos do chaincode abordados na Parte 1.
  • Você também deve ter configurado o seu ambiente de desenvolvimento chaincode go usando o v0.6 do Hyperledger Fabric, conforme descrito na Parte 1.

Consulte “Recursos para download” no final deste tutorial para baixar todos os exemplos de código, juntamente com a implementação do CustomMockStub.

O que é chaincode?

Como já falamos, Chaincode, também chamado de contrato inteligente, é um conjunto de regras/lógica de negócio escrita em uma linguagem de programação, como Golang ou Java, que conduz a forma como diferentes participantes em uma rede blockchain transacionam uns com os outros.

O que é desenvolvimento test-driven?

O desenvolvimento test-driven, ou direcionado a testes (em inglês, TDD – test-driven development), é uma metodologia de desenvolvimento na qual o desenvolvedor escreve um teste antes de escrever o código de implementação real. O desenvolvimento test-driven altera seu foco. Em vez de pensar sobre como implementar o código, você pensa sobre como validá-lo.

Em termos gerais, TDD consiste em três fases, que você repete ciclicamente até que todos os requisitos da tarefa sejam atendidos:

  1. Vermelho: Escreva um teste na ausência de qualquer código de implementação. Execute este teste, que falha.
  2. Verde: Escreva a quantidade mínima de código necessária para passar o teste. O código escrito nesta fase geralmente não é ótimo nem funcionalmente robusto.
  3. Refator: Poderia haver dois caminhos aqui. Se o código escrito na fase 2 não precisar de maior refatoração, em seguida, retorne para a fase 1 e escreva o próximo teste. Por outro lado, se o código escrito na fase 2 precisar de refatoração em termos de estrutura, funcionalidade, desempenho etc, escreva um novo teste para expor as deficiências no código e em seguida refatore esse código para passar o teste.

Pelo fato de TDD fornecer uma maneira estruturada para quebrar uma declaração de problema em pedaços menores na forma de testes, os resultados benéficos são:

  • Código bem organizado e bem projetado que é fácil de testar
  • Prova de que o seu código funciona como pretendido
  • Feedback de desenvolvimento mais rápido
  • Código de qualidade

Sobre a biblioteca de testes Golang e MockStub

Este tutorial usa a biblioteca de testes nativa fornecida por Golang para escrever testes. O teste de pacote pode ser usado para testes automatizados de pacotes Go. O pacote de teste é semelhante a um executador de teste e pode ser invocado com o comando go test.

Precisamos de uma maneira de apagar as chamadas para shim.ChaincodeStubInterface que são usadas extensivamente no desenvolvimento de chaincode. Felizmente, o pacote shim contém a implementação MockStub que pode ser usada para apagar o ChaincodeStubInterface no chaincode real enquanto testa a unidade.

Embora o MockStub contenha implementação para as funções mais comumente usadas, no Hyperledger Fabric v0.6, infelizmente, o MockStub não implementou alguns dos outros métodos, como o ReadCertAttribute, por exemplo. Uma vez que a maior parte do chaincode usaria esse método para recuperar atributos de um certificado de transação para controle de acesso, é importante ser capaz de apagar este método, bem como ser capaz de testar totalmente a unidade do nosso chaincode. Então, eu escrevi um MockStub personalizado que estende a funcionalidade shim.MockStub implementando alguns dos métodos não implementados e delegando os existentes para shim.MockStub.

Listagem 1. Trecho da implementação do CustomMockStub
package shim
 
import (
    "github.com/golang/protobuf/ptypes/timestamp"
    "github.com/hyperledger/fabric/core/chaincode/shim/crypto/attr"
)
 
type CustomMockStub struct {
    stub           *MockStub
    CertAttributes map[string][]byte
}
 
// Constructor to initialise the CustomMockStub
func NewCustomMockStub(name string, cc Chaincode, attributes map[string][]byte) *CustomMockStub {
    s := new(CustomMockStub)
    s.stub = NewMockStub(name, cc)
    s.CertAttributes = attributes
    return s
}
 
func (mock *CustomMockStub) ReadCertAttribute(attributeName string) ([]byte, error) {
    return mock.CertAttributes[attributeName], nil
}
 
func (mock *CustomMockStub) GetState(key string) ([]byte, error) {
    return mock.stub.GetState(key)
}
 
func (mock *CustomMockStub) GetTxID() string {
    return mock.stub.GetTxID()
}
 
func (mock *CustomMockStub) MockInit(uuid string, function string, args []string) ([]byte, error) {
    mock.stub.args = getBytes(function, args)
    mock.MockTransactionStart(uuid)
    bytes, err := mock.stub.cc.Init(mock, function, args)
    mock.MockTransactionEnd(uuid)
    return bytes, err
}
 
func (mock *CustomMockStub) MockInvoke(uuid string, function string, args []string) ([]byte, error) {
    mock.stub.args = getBytes(function, args)
    mock.MockTransactionStart(uuid)
    bytes, err := mock.stub.cc.Invoke(mock, function, args)
    mock.MockTransactionEnd(uuid)
    return bytes, err
}
 
func (mock *CustomMockStub) MockQuery(function string, args []string) ([]byte, error) {
    mock.stub.args = getBytes(function, args)
    // no transaction needed for queries
    bytes, err := mock.stub.cc.Query(mock, function, args)
    return bytes, err
}
 
func (mock *CustomMockStub) PutState(key string, value []byte) error {
    return mock.stub.PutState(key, value)
}
 
func (mock *CustomMockStub) MockTransactionStart(txid string) {
    mock.stub.MockTransactionStart(txid)
}
 
func (mock *CustomMockStub) MockTransactionEnd(uuid string) {
    mock.stub.MockTransactionEnd(uuid)
}

O CustomMockStub contém uma referência para o MockStub e tem um mapa de atributos que será utilizado no método ReadCertAttribute. Eu também substituí os métodos MockInit, MockQuery e MockInvoke para passar no meu CustomMockStub enquanto invocava o chaincode.

Começando

Antes de começar, certifique-se de concluir a configuração do ambiente de desenvolvimento do chaincode, seguindo as etapas da documentação do IBM Bluemix começando em “Configurando o ambiente de desenvolvimento“. Quando você chegar à seção intitulada “Configure seu pipeline de desenvolvimento”, pare lá; agora você está pronto para começar a desenvolver o chaincode no Go.

Em seguida, faça o download e descompacte o código fonte disponível no final deste artigo, em “Recursos para download”. Copie e coloque o arquivo varunmockstub.go no caminho a seguir em sua pasta Hyperledger que você configurou: $GOROOT/src/github.com/Hyperledger/fabric/core/chaincode/shim/

Neste tutorial, vamos assumir que o requisito é implementar operações CRUD para um aplicativo de empréstimo.

Crie uma pasta sample_tdd no seu ambiente de desenvolvimento Golang e crie nele os dois arquivos a seguir:

  1. go – Este arquivo representa o conjunto de testes que conterá todos os testes para sample_chaincode.go. O nome do arquivo do conjunto de teste deve estar no formato a seguir, *_test.go.
  2. go – Código de implementação real para o caso de uso do aplicativo de empréstimo imobiliário.

Vamos agora configurar o arquivo sample_chaincode_test.go. A Listagem 2 mostra as declarações de pacote e de importação.

Listagem 2. Declarações de pacote e importação em sample_chaincode_test.go
package main
import (
    "encoding/json"
    "fmt"
    "testing"
    "github.com/hyperledger/fabric/core/chaincode/shim"
)

Na Listagem 2, a linha 5 importa o pacote de teste de Go e a linha 6 importa o pacote de shim que será usado para escrever o chaincode e também inclui a implementação do CustomMockStub para testes de unidade.

Implementação de CreateLoanApplication

Vamos usar o desenvolvimento test-driven para implementar o método CreateLoanApplication no arquivo sample_chaincode.go.

Requisitos

  • O método CreateLoanApplication deve ter como entrada: um ID do aplicativo de empréstimo, uma sequência JSON que represente o aplicativo de empréstimo para criar e o ChaincodeStubInterface, que será usado para conversar com a infraestrutura Hyperledger Fabric subjacente.
  • Ele deve retornar dois parâmetros: a sequência JSON serializada representando o aplicativo de empréstimo que foi criado e um objeto de erro.
  • Ele deve lançar o erro de validação no caso de entrada inexistente/inválida.
Listagem 3. Código para o primeiro teste
func TestCreateLoanApplication (t *testing.T) {
    fmt.Println("Entering TestCreateLoanApplication")
    attributes := make(map[string][]byte)
    //Create a custom MockStub that internally uses shim.MockStub
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
}

Conforme mostrado na Listagem 3, todas as funções de teste devem começar com a palavra-chave “Testar” para que essas funções possam ser identificadas e executadas pelo pacote de testes Golang. A função de teste recebe o parâmetro testing.T que fornecerá acesso aos métodos auxiliares que podem ser usados para escrever os testes.

De acordo com os requisitos mostrados acima da Listagem 2, o método CreateLoanApplication deve receber o ChaincodeStubInterface como um dos seus parâmetros. Uma vez que a instância real do ChaincodeStubInterface é passada para os métodos Query/Invoke/Init pelo Hyperledger Fabric no runtime, você precisaria simular o ChaincodeStubInterface para teste unitário.

Na Listagem 3, a linha 5 cria um novo CustomMockStub que recebe o nome, objeto SampleChaincode (que você pretende implementar) e um mapa de atributos como entrada. O esboço sendo criado aqui é um esboço de simulação personalizado que foi discutido anteriormente.

Agora execute este teste executando go test da pasta raiz que contém o arquivo sample_chaincode_test.go. Sua saída deve ser semelhante a esta:

1 bash-3.2$ go test
2 can't load package: package .:
3 sample_chaincode.go:1:1:1 expected 'package', found 'EOF'

Como esperado, o teste falhou, uma vez que o arquivo sample_chaincode.go está vazio e nem sequer tem uma declaração de pacote. Isso representa o estágio Vermelho.

Agora, vamos escrever a quantidade mínima de código para fazer passar este teste. Adicione a seguinte linha ao arquivo sample_chaincode.go:

Listagem 4. Inclusão mínima para sample_chaincode.go passar no teste
package main

Execute o teste novamente. O teste falha com o seguinte erro:

1 ./sample_chaincode_test.go:18: undefined: SampleChaincode

O teste falha porque o arquivo sample_chaincode.go não tem o SampleChaincode definido.

Vamos adicionar este código ao arquivo sample_chaincode.go:

Listagem 5. Outra adição ao sample_chaincode.go
type SampleChaincode struct {
}

Execute o teste novamente. Ele falhará com o seguinte erro:

<code class="htmlscript plain">1 ./sample_chaincode_test.go:16: cannot use new (SampleChaincode) </code>
<code class="htmlscript plain">2 (type *SampleChaincode) as type shim.Chaincode in argument to </code>
<code class="htmlscript plain">3 shim.NewMockStub:</code>
<code class="htmlscript plain">4        *SampleChaincode does not implement shim.Chaincode </code>
<code class="htmlscript plain">5 (missing Init method)</code>

O teste falha uma vez que o CustomMockStub espera que o SampleChaincode implemente os métodos Init, Query e Invoke para ser considerado uma instância do tipo shim.Chaincode.

Agora adicione o código seguinte para sample_chaincode.go:

Listagem 6. Outra adição ao sample_chaincode.go
func (t *SampleChaincode) Init(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    return nil, nil
}
 
func (t *SampleChaincode) Query(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    return nil, nil
}
 
func (t *SampleChaincode) Invoke(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) {
    return nil, nil
}

Quando você executar o teste novamente, ele passará. Este é o estágio Verde.

<code class="htmlscript plain">1 bash-3.2$ go test</code>
<code class="htmlscript plain">2 Entering TestCreateLoanApplication</code>
<code class="htmlscript plain">3 2017/02/22 19:10:08 MockStub( mockStub &amp;{} )</code>
<code class="htmlscript plain">4 PASS</code>

Adicione o método CreateLoanApplication para sample_chaincode.go:

Listagem 7. Adicionando o método CreateLoanApplication para sample_chaincode.go
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
    return nil, nil
}

Adicione o teste seguinte para garantir que um erro de validação está sendo retornado do método CreateLoanApplication em resposta a argumentos de entrada vazios.

Listagem 8. Adicionando o teste para um erro de validação
func TestCreateLoanApplicationValidation(t *testing.T) {
    fmt.Println("Entering TestCreateLoanApplicationValidation")
    attributes := make(map[string][]byte)
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    stub.MockTransactionStart("t123")
    _, err := CreateLoanApplication(stub, []string{})
    if err == nil {
        t.Fatalf("Expected CreateLoanApplication to return validation error")
    }
    stub.MockTransactionEnd("t123")
}

Observe as invocações stub.MockTransactionStart (“t123”) e stub.MockTransactionStop (“t123”). Como qualquer escrita no ledger precisa estar em um contexto transacional, o teste deve iniciar a transação antes que o método CreateLoanApplication seja chamado, uma vez que o método CreateLoanApplication salvará o aplicativo de empréstimo no ledger. A transação com o mesmo ID deve então ser terminada para indicar conclusão.

Execute o teste usando o go test.

1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 22:55:52 MockStub( mockStub &{} )
4 Entering CreateLoanApplication
5 --- FAIL: TestCreateLoanApplicationValidation (0.00s)
6 sample_chaincode_test.go:35: Expected CreateLoanApplication to
 return validation error
7 FAIL
8 exit status 1

O teste falha como esperado. Agora adicione a quantidade mínima de código para sample_chaincode.js para que o teste passe:

Listagem 9. Adição mínima a sample_chaincode.js para passar o teste
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
    return nil, errors.New(“Expected atleast two arguments for loan application creation”)
}

Execute o teste novamente usando o go test.

1 bash-3.2$ go test
2 Entering TestCreateLoanApplication
3 2017/02/22 23:02:52 MockStub( mockStub &{} )
4 Entering CreateLoanApplication
5 PASS

O teste passa. Este é o estágio Verde pois o método CreateLoanApplication sempre retornará um erro. Agora escreva outro teste que irá expor esta deficiência e levar à refatoração do código.

Listagem 10. Um novo teste para expor a deficiência
var loanApplicationID = "la1"
var loanApplication = `{"id":"` + loanApplicationID + `","propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":16000,"otherExpenditure":0,"monthlyRent":4150,"monthlyLoanPayment":4000},"status":"Submitted","requestedAmount":40000,"fairMarketValue":58000,"approvedAmount":40000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}`
  
  func TestCreateLoanApplicationValidation2(t *testing.T) {
    fmt.Println("Entering TestCreateLoanApplicationValidation2")
    attributes := make(map[string][]byte)
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    stub.MockTransactionStart("t123")
    _, err := CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
    if err != nil {
        t.Fatalf("Expected CreateLoanApplication to succeed")
    }
    stub.MockTransactionEnd("t123")
 
}

As linhas 1 e 2 criam dados de teste para o aplicativo de empréstimo, que são usados como argumentos para o método CreateLoanApplication.

Agora execute o teste. O teste falhará, como esperado.

1 Entering TestCreateLoanApplicationValidation2
2 2017/02/22 23:09:01 MockStub( mockStub &{} )
3 Entering CreateLoanApplication
4 --- FAIL: TestCreateLoanApplicationValidation2 (0.00s)
5 sample_chaincode_test.go:55 Expected CreateLoanApplication to succeed
6 FAIL
7 exit status 1

Agora, refatore o código CreateLoanApplication em sample_chaincode.js para que este teste passe.

Listagem 11. Refatore o código CreateLoanApplication em sample_chaincode.js
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
    if len(args) < 2 {
     fmt.Println("Invalid number of args")
     return nil, errors.New("Expected atleast two arguments for loan application creation")
    }
 
    return nil, nil
}

Execute o teste novamente. Ele irá passar.

1 Entering TestCreateLoanApplicationValidation2
2 2017/03/06 12:07:34 MockStub( mockStub &{} )
3 Entering CreateLoanApplication
4 PASS

Em nosso próximo teste, precisamos validar se o aplicativo de empréstimo realmente foi criado e escrito para Blockchain. Adicione o teste seguinte ao arquivo de teste:

Listagem 12. Teste para validar se o aplicativo de empréstimo foi criado e escrito para Blockchain
func TestCreateLoanApplicationValidation3(t *testing.T) {
    fmt.Println("Entering TestCreateLoanApplicationValidation3")
    attributes := make(map[string][]byte)
    stub := shim.NewCustomMockStub("mockStub", new(SampleChaincode), attributes)
    if stub == nil {
        t.Fatalf("MockStub creation failed")
    }
 
    stub.MockTransactionStart("t123")
    CreateLoanApplication(stub, []string{loanApplicationID, loanApplication})
    stub.MockTransactionEnd("t123")
 
    var la LoanApplication
    bytes, err := stub.GetState(loanApplicationID)
    if err != nil {
        t.Fatalf("Could not fetch loan application with ID " + loanApplicationID)
    }
    err = json.Unmarshal(bytes, &la)
    if err != nil {
        t.Fatalf("Could not unmarshal loan application with ID " + loanApplicationID)
    }
    var errors = []string{}
    var loanApplicationInput LoanApplication
    err = json.Unmarshal([]byte(loanApplication), &loanApplicationInput)
    if la.ID != loanApplicationInput.ID {
        errors = append(errors, "Loan Application ID does not match")
    }
    if la.PropertyId != loanApplicationInput.PropertyId {
        errors = append(errors, "Loan Application PropertyId does not match")
    }
    if la.PersonalInfo.Firstname != loanApplicationInput.PersonalInfo.Firstname {
        errors = append(errors, "Loan Application PersonalInfo.Firstname does not match")
    }
    //Can be extended for all fields
    if len(errors) > 0 {
        t.Fatalf("Mismatch between input and stored Loan Application")
        for j := 0; j < len(errors); j++ {
            fmt.Println(errors[j])
        }
    }
}

As linhas 1-12 são consistentes com testes anteriores em termos de configuração. Na linha 14, o teste tenta recuperar o objeto de aplicativo de empréstimo que deveria ter sido criado na conclusão bem-sucedida do método CreateLoanApplication invocado na linha 10.

stub.GetState(loanApplicationID) recupera o valor da array de bytes que corresponde à chave, neste caso a ID do aplicativo de empréstimo, do ledger.

Na linha 18, o teste tenta desordenar a array de bytes recuperados na estrutura LoanApplication que pode ser interpretada e lida.

Em seguida, o teste compara o aplicativo de empréstimo recuperado com a entrada original no método CreateLoanApplication para garantir que o aplicativo de empréstimo foi mantido com os valores corretos no ledger. Incluí alguns testes que comparam certos campos. Os testes podem ser estendidos para incluir outros campos também.

Nota: Este teste ignorou a validação do esquema de entrada e foi movido para testar a persistência bem-sucedida do aplicativo de empréstimo no ledger. Idealmente, alguma forma de validação de esquema de entrada deve ser incluída no método CreateLoanApplication e testada para isso, mas para manter este tutorial conciso e gerenciável, eu passei por cima disso.

Execute o teste. Ele falhará com o seguinte erro conforme esperado.

1 2017/03/06 18:34:38 MockStub mockStub Getting la1 ()
2 --- FAIL: TestCreateLoanApplicationValidation3 (0.00s)
3 sample_chaincode_test.go:82 Could not unmarshal loan application with ID la1
4 FAIL
5 exit status 1

Agora, adicione o código ao método CreateLoanApplication que armazenará o aplicativo de empréstimo de entrada no ledger.

Listagem 13. Código para armazenar o aplicativo de empréstimo de entrada no ledger
func CreateLoanApplication(stub shim.ChaincodeStubInterface, args []string) ([]byte, error) {
    fmt.Println("Entering CreateLoanApplication")
 
    if len(args) < 2 {
        fmt.Println("Invalid number of args")
        return nil, errors.New("Expected atleast two arguments for loan application creation")
    }
 
    var loanApplicationId = args[0]
    var loanApplicationInput = args[1]
    //TODO: Include schema validation here
 
    err := stub.PutState(loanApplicationId, []byte(loanApplicationInput))
    if err != nil {
        fmt.Println("Could not save loan application to ledger", err)
        return nil, err
    }
 
    fmt.Println("Successfully saved loan application")
    return []byte(loanApplicationInput), nil
 
}

As linhas 9 e 10 recuperam a sequência JSON loanApplicationId e loanApplicationInput de args. Como mencionado anteriormente, isso deveria ser seguido pelo schema validation.

A linha 13 usa o método stub.PutState para armazenar o aplicativo de empréstimo como um chave de valor/par. O ID do aplicativo de empréstimo é armazenado como a chave e a sequência JSON do aplicativo de empréstimo é armazenada como o valor pós conversão em uma array de bytes.

Execute o teste TestCreateLoanApplicationValidation3 novamente. Ele irá passar. Concluímos o teste de unidade e o desenvolvimento do método CreateLoanApplication de acordo com os requisitos originais.

 

Encerramos aqui a terceira parte do artigo Chaincode para desenvolvedores Go. Se você não leu os artigos anteriores, leia aqui a parte 01 e a parte 02.

No próximo artigo continuaremos vendo conceitos de TDD e como aplicar esta abordagem para escrever chaincode em Golang para Hyperledger Fabric v0.6.

Recursos para download

 

***

Varun Ojha faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://www.ibm.com/developerworks/cloud/library/cl-ibm-blockchain-chaincode-testing-using-golang/index.html