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:
- Vermelho: Escreva um teste na ausência de qualquer código de implementação. Execute este teste, que falha.
- 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.
- 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:
- 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.
- 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 &{} )</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
- Code samples in this tutorial (code_package.zip | 7KB)
***
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