En este artículo, demostramos cómo estructuramos el flujo de trabajo para la plataforma 1C:Enterprise, mostramos la forma en que realizamos el aseguramiento de calidad y también compartimos algunas de las lecciones que hemos aprendido al crear uno de los sistemas de software más populares en Europa del Este.
Personas y procesos
Varios grupos de hasta 10 programadores cada uno están ocupados trabajando en la plataforma. Tres cuartas partes de ellos escriben en C++, mientras que el resto escribe en Java y JavaScript.
Cada grupo se enfoca en una línea de desarrollo separada, por ejemplo:
- Herramientas de desarrollo (Designer)
- Cliente web
- Infraestructura del servidor y clúster de conmutación por error
- y más
Hay más de una docena de grupos. También hay un grupo dedicado de aseguramiento de calidad.
Por supuesto, en un proyecto de este tamaño (más de 10 millones de líneas de código), no tiene sentido discutir la propiedad conjunta del código, ya que nunca se podría tener en cuenta una cantidad tan vasta. Estamos tratando de proporcionar el “factor de autobús” en un grupo de dos o más.
Intentamos mantener un equilibrio cuando se trata de la autonomía del equipo, lo que proporciona flexibilidad al tiempo que aumenta la velocidad de desarrollo, y la uniformidad, que garantiza una comunicación efectiva entre equipos, así como interacciones efectivas con el mundo exterior. Al final, tenemos un sistema de control de versiones común, el servidor de compilación y el rastreador de tareas (más sobre ellos a continuación), así como los estándares de codificación en C++, plantillas de documentación de proyectos, regulaciones para el manejo de informes de errores originados por los usuarios y otros aspectos. Cada equipo debe seguir las reglas desarrolladas y adoptadas por los líderes del grupo por consenso general.
Al mismo tiempo, en las prácticas “hacia adentro”, los equipos tienen bastante autonomía. Por ejemplo, las revisiones de código se utilizan en todos los comandos (y hay reglas generales que definen si se requiere una revisión), pero sus reglas internas se introdujeron en diferentes momentos y, por lo tanto, pueden diferir.
Lo mismo se aplica al flujo de trabajo. Algunos desarrolladores utilizan software ágil, mientras que otros emplean otros estilos de gestión de proyectos. El SCRUM canónico, parece, no se encuentra en ninguna parte: la especificidad de un producto empaquetado impone sus propias limitaciones. Por ejemplo, una práctica notable de demostración puede resultar inaplicable en su forma intacta. Otras prácticas, como el rol de Product Owner, son comparables con algunas de las cosas que tenemos. El líder del equipo suele actuar como Product Owner en su campo. Además del liderazgo técnico, una de las tareas más importantes para un equipo es decidir la dirección futura del desarrollo. La formulación de estrategias y tácticas de desarrollo de la plataforma es un tema interesante y complejo, y le hemos dedicado un artículo completo.
Trabajando en tareas
Cuando se toma la decisión de implementar una función, su perfil se determina en una serie de discusiones que involucran como mínimo al desarrollador responsable de la tarea y al líder del equipo. A menudo se involucra a otros miembros del equipo o miembros de otros grupos con la experiencia requerida. La versión final es luego aprobada por el liderazgo del desarrollo de la plataforma 1C:Enterprise.
Las decisiones tomadas en estas discusiones cubren:
- Qué está y qué no está incluido en el alcance de la tarea
- Cómo vemos el escenario de uso. Aún más importante es comprender qué escenarios potenciales no vamos a admitir
- Cómo se verán las interfaces de usuario
- Cómo se verá la API para el desarrollador de aplicaciones
- Cómo se combinará la nueva funcionalidad con las funcionalidades existentes
- Cómo funcionará con la seguridad
- y más
Además, la última vez intentamos discutir una tarea con un rango más amplio de clientes potenciales. Por ejemplo, en el último taller, hablamos sobre nuevas opciones para trabajar con datos binarios que aún están en la etapa de diseño, respondimos preguntas y logramos reunir una serie de escenarios de uso potenciales a partir de la discusión que nadie había propuesto antes.
Cuando se comienza a trabajar en una nueva función, se crea una tarea para ella en el rastreador de tareas. El rastreador, por cierto, está escrito en 1C:Enterprise y simplemente se llama Base de Datos de Tareas. Se almacena un documento de proyecto para cada tarea en el rastreador de tareas, que en esencia es la especificación de la tarea. Consta de tres partes principales:
- Un análisis del problema y posibles soluciones
- Una descripción de la solución a implementar
- Una descripción de los detalles técnicos de la implementación de la solución
El documento del proyecto puede ser preparado antes de su implementación, o puede comenzar después si la tarea requiere investigación o un prototipo. En cualquier caso, este es un proceso iterativo, no como un modelo de cascada; el desarrollo y la mejora del documento del proyecto se llevan a cabo al mismo tiempo que su implementación. Lo principal es que cuando la tarea se acerca a su finalización, el documento del proyecto debe ser aprobado en cada detalle. Y los detalles pueden ser numerosos, por ejemplo:
- La terminología debe ser unificada. Si el término “Guardar” se utiliza en algún lugar de la Plataforma en una situación similar, entonces debe haber una justificación seria para usar el término “Escribir”.
- Los enfoques deben ser unificados. A veces, para simplificar la investigación y la coherencia de la experiencia del usuario, se requiere repetir enfoques antiguos en nuevas tareas, incluso si hay desventajas obvias en su uso.
- Compatibilidad. En los casos en que no sea posible mantener el comportamiento antiguo, aún debemos tener en cuenta la compatibilidad. A menudo, las aplicaciones pueden incluir soluciones temporales para algunos problemas, y un cambio importante implicará la inoperabilidad por parte de los usuarios finales. Por lo tanto, a menudo mantenemos un comportamiento antiguo en “modo de compatibilidad”. Las configuraciones existentes que se ejecutan en la nueva versión de la plataforma contarán con “modo de compatibilidad” hasta que su desarrollador tome la decisión consciente de dejar de usarlo.
Además, el proyecto incluye un resumen de una discusión de la tarea para que más tarde se pueda entender por qué se aceptó o rechazó una opción u otra.
Una vez que el borrador es aprobado y el desarrollador ha implementado la nueva funcionalidad en la rama de características en SVN (o en Git, si el desarrollo se realiza en la nueva IDE), la tarea debe pasar una inspección de código y pruebas manuales por parte de otros miembros del grupo. Además, se ejecutan pruebas automatizadas en la rama de características, como se describe a continuación. En esta etapa, se crea otro documento técnico: una descripción de la tarea, dirigida a probadores y redactores técnicos. A diferencia del borrador, el documento no contiene detalles técnicos de la implementación, sino que está diseñado para permitir una comprensión rápida de qué partes de la documentación deben mejorarse, si la nueva función va acompañada de cambios incompatibles, etc. La tarea aprobada y corregida se incorpora a la rama principal de la versión y se pone a disposición del grupo de pruebas.
Lecciones y recetas
- El valor del documento de diseño, como cualquier documentación, no siempre es particularmente obvio. Para nosotros, lo siguiente es lo que le da su valor:
- Durante el proceso de diseño, ayuda a todos los involucrados a restablecer el contexto de la discusión y asegurarse de que las decisiones que se han tomado no sean descuidadas o distorsionadas.
- Más tarde, en casos dudosos en los que no estamos seguros del comportamiento adecuado, el documento del proyecto nos ayuda a recordar la decisión en sí y los fundamentos para adoptarla.
- El documento del proyecto es el punto de partida para la documentación del usuario. Los desarrolladores no necesitan escribir nada desde cero ni explicar oralmente nada a los redactores técnicos porque el documento del proyecto sirve como base.
- Siempre debemos describir escenarios de uso para la funcionalidad creada, y no en generalidades, sino en detalle: cuanto más, mejor. Si esto no se hace, la solución resultante podría ser difícil o incluso imposible de trabajar, y esto podría suceder debido a un detalle menor. En el desarrollo ágil, estos detalles son fáciles de solucionar en la próxima iteración, pero en nuestro caso, una solución podría llevar años (ciclo completo: versión final de la plataforma -> se lanza la configuración que utiliza sus innovaciones -> se recopilan los comentarios de los usuarios -> se implementan las correcciones -> se lanza una nueva versión -> se actualiza la configuración en función de las correcciones -> el usuario migra a una nueva versión de la configuración).
- Aún mejor que los escenarios, lo que realmente resulta útil es un prototipo utilizado por usuarios reales (desarrolladores de configuración) antes de que se lance oficialmente la versión y se establezca el comportamiento. Recién estamos comenzando a ampliar esta práctica, y en casi todos los casos esto ha resultado en conocimientos valiosos. A menudo, este conocimiento puede no surgir de la funcionalidad, sino aplicarse al comportamiento no funcional (por ejemplo, registro o facilidad de diagnóstico de errores).
- En la misma línea, los criterios de rendimiento deben determinarse de antemano y se debe verificar el cumplimiento de estos criterios. Antes de agregar esto a la lista de verificación de aceptación de tareas, a veces omitíamos esa parte.
Aseguramiento de calidad
En general, “calidad” y “aseguramiento de calidad” son términos muy amplios. Al menos se pueden distinguir dos procesos entre ellos: verificación y validación. La verificación generalmente se refiere al cumplimiento del comportamiento del software con las especificaciones y la ausencia de otros errores obvios, mientras que la validación se refiere a verificar el cumplimiento de las necesidades del usuario. En esta sección, nos centraremos en el aseguramiento de calidad en términos de verificación.
Los probadores solo tienen acceso a la tarea después de que se agrega a la rama principal, pero el proceso de aseguramiento de calidad comienza mucho antes. Recientemente, tuvimos que invertir esfuerzos considerables en mejorarlo, porque quedó claro que los mecanismos existentes ya no eran adecuados para el aumento del volumen de funcionalidades y su notable aumento de complejidad. Estos esfuerzos, en opinión de los socios de 1C:Enterprise con respecto a la versión 8.3.6, ya han dado resultados, pero, por supuesto, aún queda mucho trabajo por delante.
Los mecanismos existentes para el aseguramiento de calidad se pueden categorizar como organizativos o tecnológicos. Comencemos con los últimos.
Pruebas
Cuando se trata de mecanismos de aseguramiento de calidad, las pruebas son lo primero que viene a la mente. Nosotros, por supuesto, también las utilizamos, y de varias formas diferentes:
Pruebas unitarias
Escribimos pruebas unitarias en C++. Como se mencionó en el artículo anterior, utilizamos versiones derivadas de Google Test y Google Mock. Por ejemplo, una prueba típica que verifica la filtración del símbolo ampersand (“&”) en la escritura JSON podría verse así:
PRUEBA(TestEscaping, EscapeAmpersand) { // Organizar IFileExPtr archivo = create_instance<ITempFile>(SCOM_CLSIDOF(TempFile)); JSONWriterSettings configuración; configuración.escapeAmpersand = true; configuración.newLineSymbols = eJSONNewLineSymbolsNone; JSONStreamWriter::Ptr escritor = create_json_writer(archivo, &configuración); // Actuar escritor->writeStartObject(); escritor->writePropertyName(L”_&_Prop”); escritor->writeStringValue(L”_&_Value”); escritor->writeEndObject(); escritor->close(); // Afirmar std::wstring resultado = helpers::read_from_file(archivo); std::wstring esperado = std::wstring(L”{\”_\\u0026_Prop\”:\”_\\u0026_Value\”}”); ASSERT_EQ(esperado, resultado); }Pruebas integradas
El siguiente nivel de pruebas incluye pruebas de integración escritas en 1C:Enterprise. Estas son las que componen la mayor parte de nuestras pruebas. Una suite de pruebas típica es una única base de datos de información almacenada en un archivo * .dt. La infraestructura de pruebas carga esta base de datos e invoca un método preconocido en ella, que invoca pruebas separadas escritas por los desarrolladores y formatea los resultados para permitir su interpretación por la infraestructura de CI (Integración Continua).
&AtServer Procedimiento test_Array_Simple() Export NombreArchivo = GetTempFileName(“json”); NombreReferencia = “reference_Array_Simple”; Valor = CommonModule.GetSimpleArray(); EscritorJSON = GetOpenJSONWriter(NombreArchivo); EscribirJSON(EscritorJSON, Valor); EscritorJSON.Close(); CommonModule.CompareFileWithReference(NombreArchivo, NombreReferencia); EndProcedureEn este caso, si el resultado de la escritura no coincide con una referencia, se lanza una excepción. La infraestructura la intercepta e interpreta como un fallo de prueba.
Nuestro sistema de CI realiza estas pruebas para diferentes versiones de sistemas operativos y DBMS, incluyendo Windows y Linux de 32 y 64 bits, así como MS SQL Server, Oracle, PostgreSQL, IBM DB2 y nuestra base de datos de archivos propietaria.
Sistemas de prueba personalizados
La tercera y más complicada forma de pruebas son los llamados sistemas de prueba personalizados. Se utilizan cuando los escenarios que se están probando se extienden más allá de una sola base de 1C, por ejemplo, cuando se prueba la interacción con sistemas externos a través de servicios web. Para cada grupo de pruebas, se asignan una o más máquinas virtuales y se instala un software de agente especial en cada máquina. En otros aspectos, el desarrollador de pruebas tiene completa libertad y solo está limitado por el requisito de emitir el resultado como un archivo en un formato de prueba de Google que pueda ser leído por el sistema de CI.
Por ejemplo, se utiliza un servicio escrito en C# para probar un cliente de servicio web SOAP, mientras que se utiliza un marco de pruebas masivas escrito en Python para probar varias características del Diseñador.
El lado negativo de esta libertad es la necesidad de configuraciones de prueba manuales para cada sistema operativo junto con la gestión de una flota de máquinas virtuales y otros costos generales. Por lo tanto, con el desarrollo de nuestras pruebas de integración (descritas en la sección anterior), limitaremos el uso de sistemas de prueba personalizados.
Las pruebas mencionadas anteriormente son escritas por los desarrolladores de la plataforma, en C++ o creando pequeñas configuraciones (aplicaciones) diseñadas para probar funcionalidades específicas. Este es un requisito necesario para eliminar errores, pero no es suficiente, especialmente en un sistema como la plataforma 1C:Enterprise, donde la mayoría de las características no se aplican (utilizadas directamente por el usuario), sino que sirven como base para construir aplicaciones. Por lo tanto, existe un escalón adicional de pruebas: scripts de pruebas automatizadas y manuales para aplicaciones reales. Este grupo incluye pruebas de estrés, que es un tema muy amplio e interesante, por lo que dedicaremos un artículo separado a ello.
Así, se llevan a cabo todo tipo de pruebas utilizando CI. Jenkins se utiliza como servidor de integración continua.
Para cada configuración de compilación (Windows x86 y x64, Linux x86 y x64), se establece una tarea de compilación. Estas tareas se ejecutan en paralelo en diferentes máquinas. La creación de una configuración lleva mucho tiempo porque incluso en hardware potente, la compilación y vinculación de grandes volúmenes de código C++ no es tarea fácil. Además, la creación de paquetes para Linux (deb y rpm) resulta ser comparable a las compilaciones en términos de tiempo también.
Así, un “ciclo de compilación acortado” funciona durante el transcurso de un día, que verifica las compilaciones para Windows x86 y Linux x64 y ejecuta la batería mínima de pruebas, y un ciclo de compilación regular se ejecuta todas las noches, construye todas las configuraciones y ejecuta todas las pruebas. Cada compilación nocturna que se construye y prueba se marca con una etiqueta para que el desarrollador, al crear una rama para la tarea o aplicar cambios desde la rama principal, pueda estar seguro de que está trabajando con una copia compilada y funcional. Actualmente, estamos trabajando para asegurarnos de que se lance un ciclo de compilación regular con más frecuencia e incluya más pruebas. El objetivo final de este trabajo es detectar errores a través de pruebas (si se pueden detectar mediante pruebas) dentro de las dos horas posteriores a la confirmación, para que cualquier error que se detecte se corrija antes del final del día de trabajo. Este tiempo de respuesta aumenta drásticamente la eficiencia. En primer lugar, el propio desarrollador no necesita restaurar el contexto en el que estaba trabajando cuando se introdujo el error; en segundo lugar, esto reduce la probabilidad de que el error obstaculice otros trabajos en curso también.
Análisis estático y dinámico
¡El hombre no vive solo de pruebas! También utilizamos análisis estático de código, que ha demostrado su eficacia a lo largo de muchos años. Una vez a la semana localizamos al menos un error, y a menudo es el tipo de error que no se puede detectar mediante pruebas superficiales.
Utilizamos tres tipos de herramientas de análisis:
- CppCheck
- PVS-Studio
- Herramienta integrada de Microsoft Visual Studio
Todas funcionan de manera un poco diferente y localizan diferentes tipos de errores, por lo que nos gusta la forma en que se complementan entre sí.
Además de los métodos estáticos, también verificamos el comportamiento del sistema en tiempo de ejecución utilizando Address Sanitizer (parte del proyecto CLang) y Valgrind
Estas dos herramientas radicalmente diferentes se utilizan generalmente para lo mismo: encontrar disfunciones relacionadas con la memoria, como:
- acceder a memoria no inicializada
- acceder a memoria borrada
- salir más allá de los límites del arreglo, etc.
En varias ocasiones, un análisis dinámico logró encontrar errores que escaparon de los extensos esfuerzos por encontrarlos manualmente. Esto fue el impulso para organizar una ejecución automatizada por lotes de ciertos grupos de pruebas con análisis dinámico habilitado. El uso continuo de un análisis dinámico para todos los grupos de pruebas no es posible debido a limitaciones de rendimiento: Memory Sanitizer reduce el rendimiento en aproximadamente 3 veces, ¡y Valgrind reduce el rendimiento en 1-2 órdenes de magnitud! Pero incluso su uso limitado nos da buenos resultados.
Medidas organizativas de aseguramiento de calidad
Además de las pruebas automáticas realizadas por las máquinas, tratamos de incorporar el aseguramiento de calidad en el proceso diario de desarrollo.
La práctica más utilizada para este propósito es la revisión de código entre pares. En nuestra experiencia, las inspecciones constantes de código no detectan errores específicos con mucha frecuencia (aunque ocasionalmente sucede), pero evitan que surjan al proporcionar un código más legible y bien organizado, es decir, aseguran la calidad a largo plazo.
Otros objetivos implican verificaciones manuales del trabajo de los demás dentro de un grupo de programadores: resulta que incluso las pruebas superficiales realizadas por alguien que no está inmerso en la tarea ayudan a identificar errores temprano, incluso antes de que se termine la tarea.
Come tu propia comida para perros
Pero la medida organizativa más efectiva de todas es un enfoque que Microsoft llama “comer tu propia comida para perros“, donde los desarrolladores del producto son las personas que van a ser los primeros usuarios. En nuestro caso, el “producto” es nuestro rastreador de tareas (la mencionada “Base de datos de tareas”) porque los desarrolladores lo usan durante el día. Todos los días, esta configuración se migra a la última versión de la plataforma construida basada en los principios de CI, y todas las fallas y deficiencias causan una impresión inmediata en sus autores.
Nos gustaría enfatizar que la Base de Datos de Tareas es un sistema de información serio que almacena información sobre decenas de miles de tareas y problemas, y tiene más de cien usuarios. No se puede comparar con las implementaciones más grandes de 1C:Enterprise, pero es comparable a una empresa de tamaño mediano. Por supuesto, no todos los mecanismos se pueden verificar de esta manera (por ejemplo, el subsistema de contabilidad no se puede), pero para aumentar la cobertura de las funcionalidades verificadas, hay un consenso de que diferentes grupos de desarrolladores utilizan diferentes métodos de conexión; por ejemplo, algunos utilizan el cliente web, otros utilizan el cliente ligero en Windows y otros utilizan Linux. Además, se utilizan múltiples instancias del servidor de la base de datos de tareas que se ejecutan en diferentes configuraciones (diferentes versiones, diferentes sistemas operativos, etc.), se sincronizan entre sí utilizando los mecanismos incluidos en la plataforma.
Además de la Base de Datos de Tareas, hay otras bases de datos “experimentales”, pero estas son menos funcionales y no están tan cargadas.
Lecciones aprendidas
- Cuando se trata de un producto tan grande y ampliamente utilizado, es más barato escribir una prueba que no escribir una. Si hay un error en la funcionalidad y no se corrige, el costo para los usuarios finales, los socios, el soporte e incluso un departamento de desarrollo completo asociado con la reproducción, corrección y verificación posterior de errores será mucho mayor.
- Incluso si escribir pruebas automatizadas es difícil, se puede pedir al desarrollador que prepare una descripción formal de las pruebas manuales. Después de leerlo, se pueden encontrar lagunas en la forma en que el desarrollador prueba su producto, y por lo tanto, posibles errores también.
- Crear una infraestructura para CI y pruebas es un esfuerzo costoso, tanto en términos financieros como de tiempo. Esto es especialmente cierto cuando se trata de un proyecto maduro. ¡Así que asegúrese de comenzar lo antes posible!
Y otro hallazgo que no está relacionado directamente con los artículos, pero que debe compartirse de todos modos: la mejor manera de probar un marco es probar las aplicaciones construidas sobre él. Y la forma en que probamos una plataforma con aplicaciones, como 1C:Accounting, será el tema de un artículo futuro.


